diff --git a/resources/celerity/map.php b/resources/celerity/map.php
index 6f7a7a456..9f199b2ad 100644
--- a/resources/celerity/map.php
+++ b/resources/celerity/map.php
@@ -1,2238 +1,2238 @@
 <?php
 
 /**
  * This file is automatically generated. Use 'bin/celerity map' to rebuild it.
  *
  * @generated
  */
 return array(
   'names' => array(
     'core.pkg.css' => 'a50fc769',
     'core.pkg.js' => '41f5edc5',
     'darkconsole.pkg.js' => 'e7393ebb',
     'differential.pkg.css' => '49c9d302',
     'differential.pkg.js' => 'ebef29b1',
     'diffusion.pkg.css' => '385e85b3',
     'diffusion.pkg.js' => '0115b37c',
     'maniphest.pkg.css' => '4845691a',
     'maniphest.pkg.js' => '2f4f52c2',
     'rsrc/css/aphront/aphront-bars.css' => '231ac33c',
     'rsrc/css/aphront/dark-console.css' => '6378ef3d',
     'rsrc/css/aphront/dialog-view.css' => '8ea1b9cc',
     'rsrc/css/aphront/lightbox-attachment.css' => '7acac05d',
     'rsrc/css/aphront/list-filter-view.css' => 'aa5ffcb9',
     'rsrc/css/aphront/multi-column.css' => 'fd18389d',
     'rsrc/css/aphront/notification.css' => '9c279160',
     'rsrc/css/aphront/panel-view.css' => '8427b78d',
     'rsrc/css/aphront/phabricator-nav-view.css' => '0ecd30a1',
     'rsrc/css/aphront/table-view.css' => '4f2ed0bf',
     'rsrc/css/aphront/tokenizer.css' => '04875312',
     'rsrc/css/aphront/tooltip.css' => '7672b60f',
     'rsrc/css/aphront/two-column.css' => '16ab3ad2',
     'rsrc/css/aphront/typeahead-browse.css' => 'd8581d2c',
     'rsrc/css/aphront/typeahead.css' => '0e403212',
     'rsrc/css/application/almanac/almanac.css' => 'dbb9b3af',
     'rsrc/css/application/auth/auth.css' => '44975d4b',
     'rsrc/css/application/base/main-menu-view.css' => '5e8c1ab7',
     'rsrc/css/application/base/notification-menu.css' => '713df25a',
     'rsrc/css/application/base/phabricator-application-launch-view.css' => '9a233ed6',
     'rsrc/css/application/base/phui-theme.css' => '1ccdcc84',
     'rsrc/css/application/base/standard-page-view.css' => '43045fb4',
     'rsrc/css/application/calendar/calendar-icon.css' => '98ce946d',
     'rsrc/css/application/chatlog/chatlog.css' => 'f1971c1c',
     'rsrc/css/application/conduit/conduit-api.css' => '7bc725c4',
     'rsrc/css/application/config/config-options.css' => '7fedf08b',
     'rsrc/css/application/config/config-template.css' => '8e6c6fcd',
     'rsrc/css/application/config/config-welcome.css' => '6abd79be',
     'rsrc/css/application/config/setup-issue.css' => '631c4e92',
     'rsrc/css/application/config/unhandled-exception.css' => '4c96257a',
     'rsrc/css/application/conpherence/durable-column.css' => '09f1eb27',
     'rsrc/css/application/conpherence/menu.css' => 'f9f1d143',
     'rsrc/css/application/conpherence/message-pane.css' => '02d8d6aa',
     'rsrc/css/application/conpherence/notification.css' => '919974b6',
     'rsrc/css/application/conpherence/transaction.css' => '85d0974c',
     'rsrc/css/application/conpherence/update.css' => '1099a660',
     'rsrc/css/application/conpherence/widget-pane.css' => '6e0e290b',
     'rsrc/css/application/contentsource/content-source-view.css' => '4b8b05d4',
     'rsrc/css/application/countdown/timer.css' => '86b7b0a0',
     'rsrc/css/application/dashboard/dashboard.css' => 'eb458607',
     'rsrc/css/application/diff/inline-comment-summary.css' => '51efda3a',
     'rsrc/css/application/differential/add-comment.css' => 'c47f8c40',
     'rsrc/css/application/differential/changeset-view.css' => '73712bee',
     'rsrc/css/application/differential/core.css' => '7ac3cabc',
     'rsrc/css/application/differential/phui-inline-comment.css' => 'e60ad106',
     'rsrc/css/application/differential/results-table.css' => '181aa9d9',
     'rsrc/css/application/differential/revision-comment.css' => '14b8565a',
     'rsrc/css/application/differential/revision-history.css' => '0e8eb855',
     'rsrc/css/application/differential/revision-list.css' => 'f3c47d33',
     'rsrc/css/application/differential/table-of-contents.css' => '63f3ef4a',
     'rsrc/css/application/diffusion/diffusion-icons.css' => '4ba18923',
     'rsrc/css/application/diffusion/diffusion-readme.css' => '2106ea08',
     'rsrc/css/application/diffusion/diffusion-source.css' => '66fdf661',
     'rsrc/css/application/feed/feed.css' => 'ecd4ec57',
     'rsrc/css/application/files/global-drag-and-drop.css' => '697324ad',
     'rsrc/css/application/flag/flag.css' => '5337623f',
     'rsrc/css/application/harbormaster/harbormaster.css' => '49d64eb4',
     'rsrc/css/application/herald/herald-test.css' => '778b008e',
     'rsrc/css/application/herald/herald.css' => '826075fa',
     'rsrc/css/application/maniphest/batch-editor.css' => 'b0f0b6d5',
     'rsrc/css/application/maniphest/report.css' => 'f6931fdf',
     'rsrc/css/application/maniphest/task-edit.css' => '8e23031b',
     'rsrc/css/application/maniphest/task-summary.css' => '11cc5344',
     'rsrc/css/application/objectselector/object-selector.css' => '029a133d',
     'rsrc/css/application/owners/owners-path-editor.css' => '2f00933b',
     'rsrc/css/application/paste/paste.css' => '1898e534',
     'rsrc/css/application/people/people-profile.css' => '25970776',
     'rsrc/css/application/phame/phame.css' => '88bd4705',
     'rsrc/css/application/pholio/pholio-edit.css' => '3ad9d1ee',
     'rsrc/css/application/pholio/pholio-inline-comments.css' => '8e545e49',
     'rsrc/css/application/pholio/pholio.css' => '95174bdd',
     'rsrc/css/application/phortune/phortune-credit-card-form.css' => '8391eb02',
     'rsrc/css/application/phortune/phortune.css' => '9149f103',
     'rsrc/css/application/phrequent/phrequent.css' => 'ffc185ad',
     'rsrc/css/application/phriction/phriction-document-css.css' => 'd1861e06',
     'rsrc/css/application/policy/policy-edit.css' => '815c66f7',
     'rsrc/css/application/policy/policy-transaction-detail.css' => '82100a43',
     'rsrc/css/application/policy/policy.css' => '957ea14c',
     'rsrc/css/application/ponder/comments.css' => '6cdccea7',
     'rsrc/css/application/ponder/feed.css' => 'e62615b6',
     'rsrc/css/application/ponder/post.css' => '9d415218',
     'rsrc/css/application/ponder/vote.css' => '8ed6ed8b',
     'rsrc/css/application/projects/project-icon.css' => 'c2ecb7f1',
     'rsrc/css/application/releeph/releeph-core.css' => '9b3c5733',
     'rsrc/css/application/releeph/releeph-preview-branch.css' => 'b7a6f4a5',
     'rsrc/css/application/releeph/releeph-request-differential-create-dialog.css' => '8d8b92cd',
     'rsrc/css/application/releeph/releeph-request-typeahead.css' => '667a48ae',
     'rsrc/css/application/search/search-results.css' => 'ce897fb9',
     'rsrc/css/application/slowvote/slowvote.css' => '266df6a1',
     'rsrc/css/application/tokens/tokens.css' => '3d0f239e',
     'rsrc/css/application/uiexample/example.css' => '528b19de',
     'rsrc/css/core/core.css' => 'd3a3978f',
     'rsrc/css/core/remarkup.css' => '13368efd',
     'rsrc/css/core/syntax.css' => '9fd11da8',
     'rsrc/css/core/z-index.css' => '63689f49',
     'rsrc/css/diviner/diviner-shared.css' => '38813222',
     'rsrc/css/font/font-awesome.css' => 'e2e712fe',
     'rsrc/css/font/font-lato.css' => '5f05d817',
-    'rsrc/css/font/font-slabo.css' => '7a85ea13',
+    'rsrc/css/font/font-slabo.css' => '1f520937',
     'rsrc/css/font/phui-font-icon-base.css' => '3dad2ae3',
     'rsrc/css/layout/phabricator-filetree-view.css' => 'fccf9f82',
     'rsrc/css/layout/phabricator-hovercard-view.css' => '0d665853',
     'rsrc/css/layout/phabricator-side-menu-view.css' => '4f2cd343',
     'rsrc/css/layout/phabricator-source-code-view.css' => '098e9b75',
     'rsrc/css/phui/calendar/phui-calendar-day.css' => 'd1cf6f93',
     'rsrc/css/phui/calendar/phui-calendar-list.css' => 'c1c7f338',
     'rsrc/css/phui/calendar/phui-calendar-month.css' => '476be7e0',
     'rsrc/css/phui/calendar/phui-calendar.css' => 'ccabe893',
     'rsrc/css/phui/phui-action-list.css' => '32c388b3',
     'rsrc/css/phui/phui-action-panel.css' => '3ee9afd5',
     'rsrc/css/phui/phui-box.css' => 'a5bb366d',
     'rsrc/css/phui/phui-button.css' => '5ccb5ce4',
     'rsrc/css/phui/phui-crumbs-view.css' => 'ce840ec2',
     'rsrc/css/phui/phui-document.css' => '7f67a837',
     'rsrc/css/phui/phui-feed-story.css' => '153a2ebf',
     'rsrc/css/phui/phui-fontkit.css' => 'ce1ce3ca',
     'rsrc/css/phui/phui-form-view.css' => 'a0e8f168',
     'rsrc/css/phui/phui-form.css' => '1ecbc461',
     'rsrc/css/phui/phui-header-view.css' => '44fc449c',
     'rsrc/css/phui/phui-icon.css' => '88ba9081',
     'rsrc/css/phui/phui-image-mask.css' => '5a8b09c8',
     'rsrc/css/phui/phui-info-panel.css' => '27ea50a1',
     'rsrc/css/phui/phui-info-view.css' => '33e54618',
     'rsrc/css/phui/phui-list.css' => 'e448b6ba',
     'rsrc/css/phui/phui-object-box.css' => '4fd75233',
     'rsrc/css/phui/phui-object-item-list-view.css' => 'bf7463f7',
     'rsrc/css/phui/phui-pager.css' => 'bea33d23',
     'rsrc/css/phui/phui-pinboard-view.css' => '2495140e',
     'rsrc/css/phui/phui-property-list-view.css' => '1baf23eb',
     'rsrc/css/phui/phui-remarkup-preview.css' => '19ad512b',
     'rsrc/css/phui/phui-spacing.css' => '042804d6',
     'rsrc/css/phui/phui-status.css' => '888cedb8',
     'rsrc/css/phui/phui-tag-view.css' => '402691cc',
     'rsrc/css/phui/phui-text.css' => 'cf019f54',
     'rsrc/css/phui/phui-timeline-view.css' => '07a1bd0f',
     'rsrc/css/phui/phui-workboard-view.css' => '0cac51a4',
     'rsrc/css/phui/phui-workpanel-view.css' => '4bdc2562',
     'rsrc/css/sprite-login.css' => 'a3526809',
     'rsrc/css/sprite-main-header.css' => '37e05e50',
     'rsrc/css/sprite-menu.css' => 'fba663c3',
     'rsrc/css/sprite-projects.css' => 'b0d9e24f',
     'rsrc/css/sprite-tokens.css' => '1706b943',
     'rsrc/externals/font/fontawesome/fontawesome-webfont.eot' => '5fb6fb0e',
     'rsrc/externals/font/fontawesome/fontawesome-webfont.ttf' => 'a653cb11',
     'rsrc/externals/font/fontawesome/fontawesome-webfont.woff' => '80526fc8',
     'rsrc/externals/font/fontawesome/fontawesome-webfont.woff2' => '4924d54d',
     'rsrc/externals/font/lato/Lato-700.woff' => '9a1e2cb1',
     'rsrc/externals/font/lato/Lato-700.woff2' => 'ea903955',
     'rsrc/externals/font/lato/Lato-700italic.woff' => 'f7594053',
     'rsrc/externals/font/lato/Lato-700italic.woff2' => '8e92c6a0',
     'rsrc/externals/font/lato/Lato-italic.woff' => '2ff511fb',
     'rsrc/externals/font/lato/Lato-italic.woff2' => '9e7cacda',
     'rsrc/externals/font/lato/Lato-regular.woff' => 'bdba0324',
     'rsrc/externals/font/lato/Lato-regular.woff2' => '8b23fcc6',
     'rsrc/externals/font/slabo/Slabo-Regular.woff' => 'ac58aed3',
     'rsrc/externals/font/slabo/Slabo-Regular.woff2' => '02b81b15',
     'rsrc/externals/javelin/core/Event.js' => '85ea0626',
     'rsrc/externals/javelin/core/Stratcom.js' => '6c53634d',
     'rsrc/externals/javelin/core/__tests__/event-stop-and-kill.js' => '717554e4',
     'rsrc/externals/javelin/core/__tests__/install.js' => 'c432ee85',
     'rsrc/externals/javelin/core/__tests__/stratcom.js' => '88bf7313',
     'rsrc/externals/javelin/core/__tests__/util.js' => 'e251703d',
     'rsrc/externals/javelin/core/init.js' => '3010e992',
     'rsrc/externals/javelin/core/init_node.js' => 'c234aded',
     'rsrc/externals/javelin/core/install.js' => '05270951',
     'rsrc/externals/javelin/core/util.js' => '93cc50d6',
     'rsrc/externals/javelin/docs/Base.js' => '74676256',
     'rsrc/externals/javelin/docs/onload.js' => 'e819c479',
     'rsrc/externals/javelin/ext/fx/Color.js' => '7e41274a',
     'rsrc/externals/javelin/ext/fx/FX.js' => '54b612ba',
     'rsrc/externals/javelin/ext/reactor/core/DynVal.js' => 'f6555212',
     'rsrc/externals/javelin/ext/reactor/core/Reactor.js' => '2b8de964',
     'rsrc/externals/javelin/ext/reactor/core/ReactorNode.js' => '1ad0a787',
     'rsrc/externals/javelin/ext/reactor/core/ReactorNodeCalmer.js' => '76f4ebed',
     'rsrc/externals/javelin/ext/reactor/dom/RDOM.js' => 'c90a04fc',
     'rsrc/externals/javelin/ext/view/HTMLView.js' => 'fe287620',
     'rsrc/externals/javelin/ext/view/View.js' => '0f764c35',
     'rsrc/externals/javelin/ext/view/ViewInterpreter.js' => 'f829edb3',
     'rsrc/externals/javelin/ext/view/ViewPlaceholder.js' => '47830651',
     'rsrc/externals/javelin/ext/view/ViewRenderer.js' => '6c2b09a2',
     'rsrc/externals/javelin/ext/view/ViewVisitor.js' => 'efe49472',
     'rsrc/externals/javelin/ext/view/__tests__/HTMLView.js' => 'f92d7bcb',
     'rsrc/externals/javelin/ext/view/__tests__/View.js' => '6450b38b',
     'rsrc/externals/javelin/ext/view/__tests__/ViewInterpreter.js' => '7a94d6a5',
     'rsrc/externals/javelin/ext/view/__tests__/ViewRenderer.js' => '6ea96ac9',
     'rsrc/externals/javelin/lib/Cookie.js' => '62dfea03',
     'rsrc/externals/javelin/lib/DOM.js' => '147805fa',
     'rsrc/externals/javelin/lib/History.js' => 'd4505101',
     'rsrc/externals/javelin/lib/JSON.js' => '69adf288',
     'rsrc/externals/javelin/lib/Leader.js' => '331b1611',
     'rsrc/externals/javelin/lib/Mask.js' => '8a41885b',
     'rsrc/externals/javelin/lib/Quicksand.js' => '4cebc641',
     'rsrc/externals/javelin/lib/Request.js' => '94b750d2',
     'rsrc/externals/javelin/lib/Resource.js' => '44959b73',
     'rsrc/externals/javelin/lib/Routable.js' => 'b3e7d692',
     'rsrc/externals/javelin/lib/Router.js' => '29274e2b',
     'rsrc/externals/javelin/lib/Scrollbar.js' => '087e919c',
     'rsrc/externals/javelin/lib/Sound.js' => '949c0fe5',
     'rsrc/externals/javelin/lib/URI.js' => '6eff08aa',
     'rsrc/externals/javelin/lib/Vector.js' => '2caa8fb8',
     'rsrc/externals/javelin/lib/WebSocket.js' => 'e292eaf4',
     'rsrc/externals/javelin/lib/Workflow.js' => '5b2e3e2b',
     'rsrc/externals/javelin/lib/__tests__/Cookie.js' => '5ed109e8',
     'rsrc/externals/javelin/lib/__tests__/DOM.js' => 'c984504b',
     'rsrc/externals/javelin/lib/__tests__/JSON.js' => '837a7d68',
     'rsrc/externals/javelin/lib/__tests__/URI.js' => '1e45fda9',
     'rsrc/externals/javelin/lib/__tests__/behavior.js' => '1ea62783',
     'rsrc/externals/javelin/lib/behavior.js' => '61cbc29a',
     'rsrc/externals/javelin/lib/control/tokenizer/Tokenizer.js' => 'ab5f468d',
     'rsrc/externals/javelin/lib/control/typeahead/Typeahead.js' => '70baed2f',
     'rsrc/externals/javelin/lib/control/typeahead/normalizer/TypeaheadNormalizer.js' => 'e6e25838',
     'rsrc/externals/javelin/lib/control/typeahead/source/TypeaheadCompositeSource.js' => '503e17fd',
     'rsrc/externals/javelin/lib/control/typeahead/source/TypeaheadOnDemandSource.js' => '8b3fd187',
     'rsrc/externals/javelin/lib/control/typeahead/source/TypeaheadPreloadedSource.js' => '54f314a0',
     'rsrc/externals/javelin/lib/control/typeahead/source/TypeaheadSource.js' => '2818f5ce',
     'rsrc/externals/javelin/lib/control/typeahead/source/TypeaheadStaticSource.js' => '6c0e62fa',
     'rsrc/externals/raphael/g.raphael.js' => '40dde778',
     'rsrc/externals/raphael/g.raphael.line.js' => '40da039e',
     'rsrc/externals/raphael/raphael.js' => '51ee6b43',
     'rsrc/favicons/apple-touch-icon-120x120.png' => '43742962',
     'rsrc/favicons/apple-touch-icon-152x152.png' => '669eaec3',
     'rsrc/favicons/apple-touch-icon-76x76.png' => 'ecdef672',
     'rsrc/favicons/favicon-128.png' => '47cdff03',
     'rsrc/favicons/favicon-16x16.png' => 'ee2523ac',
     'rsrc/favicons/favicon-32x32.png' => 'b6a8150e',
     'rsrc/favicons/favicon-96x96.png' => '8f7ea177',
     'rsrc/image/BFCFDA.png' => 'd5ec91f4',
     'rsrc/image/actions/edit.png' => '2fc41442',
     'rsrc/image/avatar.png' => '3eb28cd9',
     'rsrc/image/checker_dark.png' => 'd8e65881',
     'rsrc/image/checker_light.png' => 'a0155918',
     'rsrc/image/checker_lighter.png' => 'd5da91b6',
     'rsrc/image/darkload.gif' => '1ffd3ec6',
     'rsrc/image/divot.png' => '94dded62',
     'rsrc/image/examples/hero.png' => '979a86ae',
     'rsrc/image/grippy_texture.png' => 'aca81e2f',
     'rsrc/image/icon/fatcow/arrow_branch.png' => '2537c01c',
     'rsrc/image/icon/fatcow/arrow_merge.png' => '21b660e0',
     'rsrc/image/icon/fatcow/bullet_black.png' => 'ff190031',
     'rsrc/image/icon/fatcow/bullet_orange.png' => 'e273e5bb',
     'rsrc/image/icon/fatcow/bullet_red.png' => 'c0b75434',
     'rsrc/image/icon/fatcow/calendar_edit.png' => '24632275',
     'rsrc/image/icon/fatcow/document_black.png' => '45fe1c60',
     'rsrc/image/icon/fatcow/flag_blue.png' => 'a01abb1d',
     'rsrc/image/icon/fatcow/flag_finish.png' => '67825cee',
     'rsrc/image/icon/fatcow/flag_ghost.png' => '20ca8783',
     'rsrc/image/icon/fatcow/flag_green.png' => '7e0eaa7a',
     'rsrc/image/icon/fatcow/flag_orange.png' => '9e73df66',
     'rsrc/image/icon/fatcow/flag_pink.png' => '7e92f3b2',
     'rsrc/image/icon/fatcow/flag_purple.png' => 'cc517522',
     'rsrc/image/icon/fatcow/flag_red.png' => '04ec726f',
     'rsrc/image/icon/fatcow/flag_yellow.png' => '73946fd4',
     'rsrc/image/icon/fatcow/folder.png' => '95a435af',
     'rsrc/image/icon/fatcow/folder_go.png' => '001cbc94',
     'rsrc/image/icon/fatcow/key_question.png' => '52a0c26a',
     'rsrc/image/icon/fatcow/link.png' => '7afd4d5e',
     'rsrc/image/icon/fatcow/page_white_edit.png' => '39a2eed8',
     'rsrc/image/icon/fatcow/page_white_link.png' => 'a90023c7',
     'rsrc/image/icon/fatcow/page_white_put.png' => '08c95a0c',
     'rsrc/image/icon/fatcow/page_white_text.png' => '1e1f79c3',
     'rsrc/image/icon/fatcow/source/conduit.png' => '4ea01d2f',
     'rsrc/image/icon/fatcow/source/email.png' => '9bab3239',
     'rsrc/image/icon/fatcow/source/fax.png' => '04195e68',
     'rsrc/image/icon/fatcow/source/mobile.png' => 'f1321264',
     'rsrc/image/icon/fatcow/source/tablet.png' => '49396799',
     'rsrc/image/icon/fatcow/source/web.png' => '136ccb5d',
     'rsrc/image/icon/lightbox/close-2.png' => 'cc40e7c8',
     'rsrc/image/icon/lightbox/close-hover-2.png' => 'fb5d6d9e',
     'rsrc/image/icon/lightbox/left-arrow-2.png' => '8426133b',
     'rsrc/image/icon/lightbox/left-arrow-hover-2.png' => '701e5ee3',
     'rsrc/image/icon/lightbox/right-arrow-2.png' => '6d5519a0',
     'rsrc/image/icon/lightbox/right-arrow-hover-2.png' => '3a04aa21',
     'rsrc/image/icon/subscribe.png' => 'd03ed5a5',
     'rsrc/image/icon/tango/attachment.png' => 'ecc8022e',
     'rsrc/image/icon/tango/edit.png' => '929a1363',
     'rsrc/image/icon/tango/go-down.png' => '96d95e43',
     'rsrc/image/icon/tango/log.png' => 'b08cc63a',
     'rsrc/image/icon/tango/upload.png' => '7bbb7984',
     'rsrc/image/icon/unsubscribe.png' => '25725013',
     'rsrc/image/lightblue-header.png' => '5c168b6d',
     'rsrc/image/main_texture.png' => '29a2c5ad',
     'rsrc/image/menu_texture.png' => '5a17580d',
     'rsrc/image/people/harding.png' => '45aa614e',
     'rsrc/image/people/jefferson.png' => 'afca0e53',
     'rsrc/image/people/lincoln.png' => '9369126d',
     'rsrc/image/people/mckinley.png' => 'fb8f16ce',
     'rsrc/image/people/taft.png' => 'd7bc402c',
     'rsrc/image/people/washington.png' => '40dd301c',
     'rsrc/image/phrequent_active.png' => 'a466a8ed',
     'rsrc/image/phrequent_inactive.png' => 'bfc15a69',
     'rsrc/image/sprite-login-X2.png' => 'a15918f0',
     'rsrc/image/sprite-login.png' => '8cee4f6e',
     'rsrc/image/sprite-main-header.png' => 'f9008250',
     'rsrc/image/sprite-menu-X2.png' => '1c25965b',
     'rsrc/image/sprite-menu.png' => '76373b62',
     'rsrc/image/sprite-projects-X2.png' => '8c91c839',
     'rsrc/image/sprite-projects.png' => 'ef9dc9b5',
     'rsrc/image/sprite-tokens-X2.png' => 'b4776580',
     'rsrc/image/sprite-tokens.png' => '25b75533',
     'rsrc/image/texture/card-gradient.png' => '815f26e8',
     'rsrc/image/texture/dark-menu-hover.png' => '5fa7ece8',
     'rsrc/image/texture/dark-menu.png' => '7e22296e',
     'rsrc/image/texture/grip.png' => '719404f3',
     'rsrc/image/texture/panel-header-gradient.png' => 'e3b8dcfe',
     'rsrc/image/texture/phlnx-bg.png' => '8d819209',
     'rsrc/image/texture/pholio-background.gif' => 'ba29239c',
     'rsrc/image/texture/table_header.png' => '5c433037',
     'rsrc/image/texture/table_header_hover.png' => '038ec3b9',
     'rsrc/image/texture/table_header_tall.png' => 'd56b434f',
     'rsrc/js/application/aphlict/Aphlict.js' => '5359e785',
     'rsrc/js/application/aphlict/behavior-aphlict-dropdown.js' => '031cee25',
     'rsrc/js/application/aphlict/behavior-aphlict-listen.js' => 'b1a59974',
     'rsrc/js/application/aphlict/behavior-aphlict-status.js' => 'ea681761',
     'rsrc/js/application/auth/behavior-persona-login.js' => '9414ff18',
     'rsrc/js/application/calendar/behavior-day-view.js' => '5c46cff2',
     'rsrc/js/application/calendar/behavior-event-all-day.js' => '38dcf3c8',
     'rsrc/js/application/calendar/behavior-recurring-edit.js' => '5f1c4d5f',
     'rsrc/js/application/config/behavior-reorder-fields.js' => 'b6993408',
     'rsrc/js/application/conpherence/ConpherenceThreadManager.js' => '01774ab2',
     'rsrc/js/application/conpherence/behavior-drag-and-drop-photo.js' => 'cf86d16a',
     'rsrc/js/application/conpherence/behavior-durable-column.js' => 'c72aa091',
     'rsrc/js/application/conpherence/behavior-menu.js' => 'd3782c93',
     'rsrc/js/application/conpherence/behavior-pontificate.js' => '21ba5861',
     'rsrc/js/application/conpherence/behavior-quicksand-blacklist.js' => '7927a7d3',
     'rsrc/js/application/conpherence/behavior-widget-pane.js' => '93568464',
     'rsrc/js/application/countdown/timer.js' => 'e4cc26b3',
     'rsrc/js/application/dashboard/behavior-dashboard-async-panel.js' => '469c0d9e',
     'rsrc/js/application/dashboard/behavior-dashboard-move-panels.js' => '82439934',
     'rsrc/js/application/dashboard/behavior-dashboard-query-panel-select.js' => '453c5375',
     'rsrc/js/application/dashboard/behavior-dashboard-tab-panel.js' => 'd4eecc63',
     'rsrc/js/application/differential/ChangesetViewManager.js' => '58562350',
     'rsrc/js/application/differential/DifferentialInlineCommentEditor.js' => 'd4c87bf4',
     'rsrc/js/application/differential/behavior-add-reviewers-and-ccs.js' => 'e10f8e18',
     'rsrc/js/application/differential/behavior-comment-jump.js' => '4fdb476d',
     'rsrc/js/application/differential/behavior-comment-preview.js' => 'b064af76',
     'rsrc/js/application/differential/behavior-diff-radios.js' => 'e1ff79b1',
     'rsrc/js/application/differential/behavior-dropdown-menus.js' => '2035b9cb',
     'rsrc/js/application/differential/behavior-edit-inline-comments.js' => '037b59eb',
     'rsrc/js/application/differential/behavior-keyboard-nav.js' => '2c426492',
     'rsrc/js/application/differential/behavior-populate.js' => '8694b1df',
     'rsrc/js/application/differential/behavior-show-field-details.js' => 'bba9eedf',
     'rsrc/js/application/differential/behavior-toggle-files.js' => 'ca3f91eb',
     'rsrc/js/application/differential/behavior-user-select.js' => 'a8d8459d',
     'rsrc/js/application/diffusion/DiffusionLocateFileSource.js' => 'b42eddc7',
     'rsrc/js/application/diffusion/behavior-audit-preview.js' => 'd835b03a',
     'rsrc/js/application/diffusion/behavior-commit-branches.js' => 'bdaf4d04',
     'rsrc/js/application/diffusion/behavior-commit-graph.js' => '9007c197',
     'rsrc/js/application/diffusion/behavior-jump-to.js' => '73d09eef',
     'rsrc/js/application/diffusion/behavior-load-blame.js' => '42126667',
     'rsrc/js/application/diffusion/behavior-locate-file.js' => '6d3e1947',
     'rsrc/js/application/diffusion/behavior-pull-lastmodified.js' => '2b228192',
     'rsrc/js/application/doorkeeper/behavior-doorkeeper-tag.js' => 'e5822781',
     'rsrc/js/application/files/behavior-icon-composer.js' => '8ef9ab58',
     'rsrc/js/application/files/behavior-launch-icon-composer.js' => '48086888',
-    'rsrc/js/application/herald/HeraldRuleEditor.js' => '271ffdd7',
+    'rsrc/js/application/herald/HeraldRuleEditor.js' => 'b2cae298',
     'rsrc/js/application/herald/PathTypeahead.js' => 'f7fc67ec',
     'rsrc/js/application/herald/herald-rule-editor.js' => '7ebaeed3',
-    'rsrc/js/application/maniphest/behavior-batch-editor.js' => 'f5d1233b',
+    'rsrc/js/application/maniphest/behavior-batch-editor.js' => '782ab6e7',
     'rsrc/js/application/maniphest/behavior-batch-selector.js' => '7b98d7c5',
     'rsrc/js/application/maniphest/behavior-line-chart.js' => '88f0c5b3',
     'rsrc/js/application/maniphest/behavior-list-edit.js' => 'a9f88de2',
     'rsrc/js/application/maniphest/behavior-subpriorityeditor.js' => '84845b5b',
     'rsrc/js/application/maniphest/behavior-transaction-controls.js' => '44168bad',
     'rsrc/js/application/maniphest/behavior-transaction-expand.js' => '5fefb143',
     'rsrc/js/application/maniphest/behavior-transaction-preview.js' => '4c95d29e',
     'rsrc/js/application/owners/OwnersPathEditor.js' => 'aa1733d0',
     'rsrc/js/application/owners/owners-path-editor.js' => '7a68dda3',
     'rsrc/js/application/passphrase/passphrase-credential-control.js' => '3cb0b2fc',
     'rsrc/js/application/phame/phame-post-preview.js' => 'be807912',
     'rsrc/js/application/pholio/behavior-pholio-mock-edit.js' => '246dc085',
     'rsrc/js/application/pholio/behavior-pholio-mock-view.js' => 'fbe497e7',
     'rsrc/js/application/phortune/behavior-stripe-payment-form.js' => '3f5d6dbf',
     'rsrc/js/application/phortune/behavior-test-payment-form.js' => 'fc91ab6c',
     'rsrc/js/application/phortune/phortune-credit-card-form.js' => '2290aeef',
-    'rsrc/js/application/policy/behavior-policy-control.js' => '9a340b3d',
+    'rsrc/js/application/policy/behavior-policy-control.js' => '7d470398',
     'rsrc/js/application/policy/behavior-policy-rule-editor.js' => '5e9f347c',
     'rsrc/js/application/ponder/behavior-votebox.js' => '4e9b766b',
     'rsrc/js/application/projects/behavior-project-boards.js' => 'ba4fa35c',
     'rsrc/js/application/projects/behavior-project-create.js' => '065227cc',
     'rsrc/js/application/projects/behavior-reorder-columns.js' => 'e1d25dfb',
     'rsrc/js/application/releeph/releeph-preview-branch.js' => 'b2b4fbaf',
     'rsrc/js/application/releeph/releeph-request-state-change.js' => 'a0b57eb8',
     'rsrc/js/application/releeph/releeph-request-typeahead.js' => 'de2e896f',
     'rsrc/js/application/repository/repository-crossreference.js' => 'bea81850',
     'rsrc/js/application/search/behavior-reorder-queries.js' => 'e9581f08',
     'rsrc/js/application/slowvote/behavior-slowvote-embed.js' => '887ad43f',
     'rsrc/js/application/transactions/behavior-show-older-transactions.js' => 'dbbf48b6',
     'rsrc/js/application/transactions/behavior-transaction-comment-form.js' => 'b23b49e6',
     'rsrc/js/application/transactions/behavior-transaction-list.js' => '13c739ea',
     'rsrc/js/application/typeahead/behavior-typeahead-browse.js' => '635de1ec',
     'rsrc/js/application/typeahead/behavior-typeahead-search.js' => '93d0c9e3',
     'rsrc/js/application/uiexample/JavelinViewExample.js' => 'd4a14807',
     'rsrc/js/application/uiexample/ReactorButtonExample.js' => 'd19198c8',
     'rsrc/js/application/uiexample/ReactorCheckboxExample.js' => '519705ea',
     'rsrc/js/application/uiexample/ReactorFocusExample.js' => '40a6a403',
     'rsrc/js/application/uiexample/ReactorInputExample.js' => '886fd850',
     'rsrc/js/application/uiexample/ReactorMouseoverExample.js' => '47c794d8',
     'rsrc/js/application/uiexample/ReactorRadioExample.js' => '988040b4',
     'rsrc/js/application/uiexample/ReactorSelectExample.js' => 'a155550f',
     'rsrc/js/application/uiexample/ReactorSendClassExample.js' => '1def2711',
     'rsrc/js/application/uiexample/ReactorSendPropertiesExample.js' => 'b1f0ccee',
     'rsrc/js/application/uiexample/busy-example.js' => '60479091',
     'rsrc/js/application/uiexample/gesture-example.js' => '558829c2',
     'rsrc/js/application/uiexample/notification-example.js' => '8ce821c5',
     'rsrc/js/core/Busy.js' => '59a7976a',
     'rsrc/js/core/DragAndDropFileUpload.js' => '07de8873',
     'rsrc/js/core/DraggableList.js' => 'a16ec1c6',
     'rsrc/js/core/FileUpload.js' => '477359c8',
     'rsrc/js/core/Hovercard.js' => '14ac66f5',
     'rsrc/js/core/KeyboardShortcut.js' => '1ae869f2',
     'rsrc/js/core/KeyboardShortcutManager.js' => 'c1700f6f',
     'rsrc/js/core/MultirowRowManager.js' => 'b5d57730',
     'rsrc/js/core/Notification.js' => '0c6946e7',
     'rsrc/js/core/Prefab.js' => '6920d200',
     'rsrc/js/core/ShapedRequest.js' => '7cbe244b',
     'rsrc/js/core/TextAreaUtils.js' => '5c93c52c',
     'rsrc/js/core/Title.js' => 'df5e11d2',
     'rsrc/js/core/ToolTip.js' => '1d298e3a',
     'rsrc/js/core/behavior-active-nav.js' => 'e379b58e',
     'rsrc/js/core/behavior-audio-source.js' => '59b251eb',
     'rsrc/js/core/behavior-autofocus.js' => '7319e029',
     'rsrc/js/core/behavior-choose-control.js' => '6153c708',
     'rsrc/js/core/behavior-crop.js' => 'fa0f4fc2',
     'rsrc/js/core/behavior-dark-console.js' => 'f411b6ae',
     'rsrc/js/core/behavior-device.js' => 'a205cf28',
     'rsrc/js/core/behavior-drag-and-drop-textarea.js' => '6d49590e',
     'rsrc/js/core/behavior-error-log.js' => '6882e80a',
     'rsrc/js/core/behavior-fancy-datepicker.js' => '510b5809',
     'rsrc/js/core/behavior-file-tree.js' => '88236f00',
     'rsrc/js/core/behavior-form.js' => '5c54cbf3',
     'rsrc/js/core/behavior-gesture.js' => '3ab51e2c',
     'rsrc/js/core/behavior-global-drag-and-drop.js' => 'c8e57404',
     'rsrc/js/core/behavior-high-security-warning.js' => 'a464fe03',
     'rsrc/js/core/behavior-history-install.js' => '7ee2b591',
     'rsrc/js/core/behavior-hovercard.js' => 'f36e01af',
     'rsrc/js/core/behavior-keyboard-pager.js' => 'a8da01f0',
     'rsrc/js/core/behavior-keyboard-shortcuts.js' => 'd75709e6',
     'rsrc/js/core/behavior-lightbox-attachments.js' => 'f8ba29d7',
     'rsrc/js/core/behavior-line-linker.js' => '1499a8cb',
     'rsrc/js/core/behavior-more.js' => 'a80d0378',
     'rsrc/js/core/behavior-object-selector.js' => '49b73b36',
     'rsrc/js/core/behavior-oncopy.js' => '2926fff2',
     'rsrc/js/core/behavior-phabricator-nav.js' => '56a1ca03',
     'rsrc/js/core/behavior-phabricator-remarkup-assist.js' => '095ed313',
     'rsrc/js/core/behavior-refresh-csrf.js' => '7814b593',
     'rsrc/js/core/behavior-remarkup-preview.js' => 'f7379f45',
     'rsrc/js/core/behavior-reorder-applications.js' => '76b9fc3e',
     'rsrc/js/core/behavior-reveal-content.js' => '60821bc7',
     'rsrc/js/core/behavior-scrollbar.js' => '834a1173',
     'rsrc/js/core/behavior-search-typeahead.js' => '048330fa',
     'rsrc/js/core/behavior-select-on-click.js' => '4e3e79a6',
     'rsrc/js/core/behavior-time-typeahead.js' => '8bfbb401',
     'rsrc/js/core/behavior-toggle-class.js' => '5d7c9f33',
     'rsrc/js/core/behavior-tokenizer.js' => 'b3a4b884',
     'rsrc/js/core/behavior-tooltip.js' => '3ee3408b',
     'rsrc/js/core/behavior-watch-anchor.js' => '9f36c42d',
     'rsrc/js/core/behavior-workflow.js' => '0a3f3021',
     'rsrc/js/core/phtize.js' => 'd254d646',
     'rsrc/js/phui/behavior-phui-dropdown-menu.js' => '54733475',
     'rsrc/js/phui/behavior-phui-object-box-tabs.js' => '2bfa2836',
     'rsrc/js/phuix/PHUIXActionListView.js' => 'b5c256b8',
     'rsrc/js/phuix/PHUIXActionView.js' => '8cf6d262',
     'rsrc/js/phuix/PHUIXDropdownMenu.js' => 'bd4c8dca',
   ),
   'symbols' => array(
     'almanac-css' => 'dbb9b3af',
     'aphront-bars' => '231ac33c',
     'aphront-dark-console-css' => '6378ef3d',
     'aphront-dialog-view-css' => '8ea1b9cc',
     'aphront-list-filter-view-css' => 'aa5ffcb9',
     'aphront-multi-column-view-css' => 'fd18389d',
     'aphront-panel-view-css' => '8427b78d',
     'aphront-table-view-css' => '4f2ed0bf',
     'aphront-tokenizer-control-css' => '04875312',
     'aphront-tooltip-css' => '7672b60f',
     'aphront-two-column-view-css' => '16ab3ad2',
     'aphront-typeahead-control-css' => '0e403212',
     'auth-css' => '44975d4b',
     'calendar-icon-css' => '98ce946d',
     'changeset-view-manager' => '58562350',
     'conduit-api-css' => '7bc725c4',
     'config-options-css' => '7fedf08b',
     'config-welcome-css' => '6abd79be',
     'conpherence-durable-column-view' => '09f1eb27',
     'conpherence-menu-css' => 'f9f1d143',
     'conpherence-message-pane-css' => '02d8d6aa',
     'conpherence-notification-css' => '919974b6',
     'conpherence-thread-manager' => '01774ab2',
     'conpherence-transaction-css' => '85d0974c',
     'conpherence-update-css' => '1099a660',
     'conpherence-widget-pane-css' => '6e0e290b',
     'differential-changeset-view-css' => '73712bee',
     'differential-core-view-css' => '7ac3cabc',
     'differential-inline-comment-editor' => 'd4c87bf4',
     'differential-results-table-css' => '181aa9d9',
     'differential-revision-add-comment-css' => 'c47f8c40',
     'differential-revision-comment-css' => '14b8565a',
     'differential-revision-history-css' => '0e8eb855',
     'differential-revision-list-css' => 'f3c47d33',
     'differential-table-of-contents-css' => '63f3ef4a',
     'diffusion-icons-css' => '4ba18923',
     'diffusion-readme-css' => '2106ea08',
     'diffusion-source-css' => '66fdf661',
     'diviner-shared-css' => '38813222',
     'font-fontawesome' => 'e2e712fe',
     'font-lato' => '5f05d817',
-    'font-slabo' => '7a85ea13',
+    'font-slabo' => '1f520937',
     'global-drag-and-drop-css' => '697324ad',
     'harbormaster-css' => '49d64eb4',
     'herald-css' => '826075fa',
-    'herald-rule-editor' => '271ffdd7',
+    'herald-rule-editor' => 'b2cae298',
     'herald-test-css' => '778b008e',
     'inline-comment-summary-css' => '51efda3a',
     'javelin-aphlict' => '5359e785',
     'javelin-behavior' => '61cbc29a',
     'javelin-behavior-aphlict-dropdown' => '031cee25',
     'javelin-behavior-aphlict-listen' => 'b1a59974',
     'javelin-behavior-aphlict-status' => 'ea681761',
     'javelin-behavior-aphront-basic-tokenizer' => 'b3a4b884',
     'javelin-behavior-aphront-crop' => 'fa0f4fc2',
     'javelin-behavior-aphront-drag-and-drop-textarea' => '6d49590e',
     'javelin-behavior-aphront-form-disable-on-submit' => '5c54cbf3',
     'javelin-behavior-aphront-more' => 'a80d0378',
     'javelin-behavior-audio-source' => '59b251eb',
     'javelin-behavior-audit-preview' => 'd835b03a',
     'javelin-behavior-choose-control' => '6153c708',
     'javelin-behavior-config-reorder-fields' => 'b6993408',
     'javelin-behavior-conpherence-drag-and-drop-photo' => 'cf86d16a',
     'javelin-behavior-conpherence-menu' => 'd3782c93',
     'javelin-behavior-conpherence-pontificate' => '21ba5861',
     'javelin-behavior-conpherence-widget-pane' => '93568464',
     'javelin-behavior-countdown-timer' => 'e4cc26b3',
     'javelin-behavior-dark-console' => 'f411b6ae',
     'javelin-behavior-dashboard-async-panel' => '469c0d9e',
     'javelin-behavior-dashboard-move-panels' => '82439934',
     'javelin-behavior-dashboard-query-panel-select' => '453c5375',
     'javelin-behavior-dashboard-tab-panel' => 'd4eecc63',
     'javelin-behavior-day-view' => '5c46cff2',
     'javelin-behavior-device' => 'a205cf28',
     'javelin-behavior-differential-add-reviewers-and-ccs' => 'e10f8e18',
     'javelin-behavior-differential-comment-jump' => '4fdb476d',
     'javelin-behavior-differential-diff-radios' => 'e1ff79b1',
     'javelin-behavior-differential-dropdown-menus' => '2035b9cb',
     'javelin-behavior-differential-edit-inline-comments' => '037b59eb',
     'javelin-behavior-differential-feedback-preview' => 'b064af76',
     'javelin-behavior-differential-keyboard-navigation' => '2c426492',
     'javelin-behavior-differential-populate' => '8694b1df',
     'javelin-behavior-differential-show-field-details' => 'bba9eedf',
     'javelin-behavior-differential-toggle-files' => 'ca3f91eb',
     'javelin-behavior-differential-user-select' => 'a8d8459d',
     'javelin-behavior-diffusion-commit-branches' => 'bdaf4d04',
     'javelin-behavior-diffusion-commit-graph' => '9007c197',
     'javelin-behavior-diffusion-jump-to' => '73d09eef',
     'javelin-behavior-diffusion-locate-file' => '6d3e1947',
     'javelin-behavior-diffusion-pull-lastmodified' => '2b228192',
     'javelin-behavior-doorkeeper-tag' => 'e5822781',
     'javelin-behavior-durable-column' => 'c72aa091',
     'javelin-behavior-error-log' => '6882e80a',
     'javelin-behavior-event-all-day' => '38dcf3c8',
     'javelin-behavior-fancy-datepicker' => '510b5809',
     'javelin-behavior-global-drag-and-drop' => 'c8e57404',
     'javelin-behavior-herald-rule-editor' => '7ebaeed3',
     'javelin-behavior-high-security-warning' => 'a464fe03',
     'javelin-behavior-history-install' => '7ee2b591',
     'javelin-behavior-icon-composer' => '8ef9ab58',
     'javelin-behavior-launch-icon-composer' => '48086888',
     'javelin-behavior-lightbox-attachments' => 'f8ba29d7',
     'javelin-behavior-line-chart' => '88f0c5b3',
     'javelin-behavior-load-blame' => '42126667',
-    'javelin-behavior-maniphest-batch-editor' => 'f5d1233b',
+    'javelin-behavior-maniphest-batch-editor' => '782ab6e7',
     'javelin-behavior-maniphest-batch-selector' => '7b98d7c5',
     'javelin-behavior-maniphest-list-editor' => 'a9f88de2',
     'javelin-behavior-maniphest-subpriority-editor' => '84845b5b',
     'javelin-behavior-maniphest-transaction-controls' => '44168bad',
     'javelin-behavior-maniphest-transaction-expand' => '5fefb143',
     'javelin-behavior-maniphest-transaction-preview' => '4c95d29e',
     'javelin-behavior-owners-path-editor' => '7a68dda3',
     'javelin-behavior-passphrase-credential-control' => '3cb0b2fc',
     'javelin-behavior-persona-login' => '9414ff18',
     'javelin-behavior-phabricator-active-nav' => 'e379b58e',
     'javelin-behavior-phabricator-autofocus' => '7319e029',
     'javelin-behavior-phabricator-busy-example' => '60479091',
     'javelin-behavior-phabricator-file-tree' => '88236f00',
     'javelin-behavior-phabricator-gesture' => '3ab51e2c',
     'javelin-behavior-phabricator-gesture-example' => '558829c2',
     'javelin-behavior-phabricator-hovercards' => 'f36e01af',
     'javelin-behavior-phabricator-keyboard-pager' => 'a8da01f0',
     'javelin-behavior-phabricator-keyboard-shortcuts' => 'd75709e6',
     'javelin-behavior-phabricator-line-linker' => '1499a8cb',
     'javelin-behavior-phabricator-nav' => '56a1ca03',
     'javelin-behavior-phabricator-notification-example' => '8ce821c5',
     'javelin-behavior-phabricator-object-selector' => '49b73b36',
     'javelin-behavior-phabricator-oncopy' => '2926fff2',
     'javelin-behavior-phabricator-remarkup-assist' => '095ed313',
     'javelin-behavior-phabricator-reveal-content' => '60821bc7',
     'javelin-behavior-phabricator-search-typeahead' => '048330fa',
     'javelin-behavior-phabricator-show-older-transactions' => 'dbbf48b6',
     'javelin-behavior-phabricator-tooltips' => '3ee3408b',
     'javelin-behavior-phabricator-transaction-comment-form' => 'b23b49e6',
     'javelin-behavior-phabricator-transaction-list' => '13c739ea',
     'javelin-behavior-phabricator-watch-anchor' => '9f36c42d',
     'javelin-behavior-phame-post-preview' => 'be807912',
     'javelin-behavior-pholio-mock-edit' => '246dc085',
     'javelin-behavior-pholio-mock-view' => 'fbe497e7',
     'javelin-behavior-phui-dropdown-menu' => '54733475',
     'javelin-behavior-phui-object-box-tabs' => '2bfa2836',
-    'javelin-behavior-policy-control' => '9a340b3d',
+    'javelin-behavior-policy-control' => '7d470398',
     'javelin-behavior-policy-rule-editor' => '5e9f347c',
     'javelin-behavior-ponder-votebox' => '4e9b766b',
     'javelin-behavior-project-boards' => 'ba4fa35c',
     'javelin-behavior-project-create' => '065227cc',
     'javelin-behavior-quicksand-blacklist' => '7927a7d3',
     'javelin-behavior-recurring-edit' => '5f1c4d5f',
     'javelin-behavior-refresh-csrf' => '7814b593',
     'javelin-behavior-releeph-preview-branch' => 'b2b4fbaf',
     'javelin-behavior-releeph-request-state-change' => 'a0b57eb8',
     'javelin-behavior-releeph-request-typeahead' => 'de2e896f',
     'javelin-behavior-remarkup-preview' => 'f7379f45',
     'javelin-behavior-reorder-applications' => '76b9fc3e',
     'javelin-behavior-reorder-columns' => 'e1d25dfb',
     'javelin-behavior-repository-crossreference' => 'bea81850',
     'javelin-behavior-scrollbar' => '834a1173',
     'javelin-behavior-search-reorder-queries' => 'e9581f08',
     'javelin-behavior-select-on-click' => '4e3e79a6',
     'javelin-behavior-slowvote-embed' => '887ad43f',
     'javelin-behavior-stripe-payment-form' => '3f5d6dbf',
     'javelin-behavior-test-payment-form' => 'fc91ab6c',
     'javelin-behavior-time-typeahead' => '8bfbb401',
     'javelin-behavior-toggle-class' => '5d7c9f33',
     'javelin-behavior-typeahead-browse' => '635de1ec',
     'javelin-behavior-typeahead-search' => '93d0c9e3',
     'javelin-behavior-view-placeholder' => '47830651',
     'javelin-behavior-workflow' => '0a3f3021',
     'javelin-color' => '7e41274a',
     'javelin-cookie' => '62dfea03',
     'javelin-diffusion-locate-file-source' => 'b42eddc7',
     'javelin-dom' => '147805fa',
     'javelin-dynval' => 'f6555212',
     'javelin-event' => '85ea0626',
     'javelin-fx' => '54b612ba',
     'javelin-history' => 'd4505101',
     'javelin-install' => '05270951',
     'javelin-json' => '69adf288',
     'javelin-leader' => '331b1611',
     'javelin-magical-init' => '3010e992',
     'javelin-mask' => '8a41885b',
     'javelin-quicksand' => '4cebc641',
     'javelin-reactor' => '2b8de964',
     'javelin-reactor-dom' => 'c90a04fc',
     'javelin-reactor-node-calmer' => '76f4ebed',
     'javelin-reactornode' => '1ad0a787',
     'javelin-request' => '94b750d2',
     'javelin-resource' => '44959b73',
     'javelin-routable' => 'b3e7d692',
     'javelin-router' => '29274e2b',
     'javelin-scrollbar' => '087e919c',
     'javelin-sound' => '949c0fe5',
     'javelin-stratcom' => '6c53634d',
     'javelin-tokenizer' => 'ab5f468d',
     'javelin-typeahead' => '70baed2f',
     'javelin-typeahead-composite-source' => '503e17fd',
     'javelin-typeahead-normalizer' => 'e6e25838',
     'javelin-typeahead-ondemand-source' => '8b3fd187',
     'javelin-typeahead-preloaded-source' => '54f314a0',
     'javelin-typeahead-source' => '2818f5ce',
     'javelin-typeahead-static-source' => '6c0e62fa',
     'javelin-uri' => '6eff08aa',
     'javelin-util' => '93cc50d6',
     'javelin-vector' => '2caa8fb8',
     'javelin-view' => '0f764c35',
     'javelin-view-html' => 'fe287620',
     'javelin-view-interpreter' => 'f829edb3',
     'javelin-view-renderer' => '6c2b09a2',
     'javelin-view-visitor' => 'efe49472',
     'javelin-websocket' => 'e292eaf4',
     'javelin-workflow' => '5b2e3e2b',
     'lightbox-attachment-css' => '7acac05d',
     'maniphest-batch-editor' => 'b0f0b6d5',
     'maniphest-report-css' => 'f6931fdf',
     'maniphest-task-edit-css' => '8e23031b',
     'maniphest-task-summary-css' => '11cc5344',
     'multirow-row-manager' => 'b5d57730',
     'owners-path-editor' => 'aa1733d0',
     'owners-path-editor-css' => '2f00933b',
     'paste-css' => '1898e534',
     'path-typeahead' => 'f7fc67ec',
     'people-profile-css' => '25970776',
     'phabricator-action-list-view-css' => '32c388b3',
     'phabricator-application-launch-view-css' => '9a233ed6',
     'phabricator-busy' => '59a7976a',
     'phabricator-chatlog-css' => 'f1971c1c',
     'phabricator-content-source-view-css' => '4b8b05d4',
     'phabricator-core-css' => 'd3a3978f',
     'phabricator-countdown-css' => '86b7b0a0',
     'phabricator-dashboard-css' => 'eb458607',
     'phabricator-drag-and-drop-file-upload' => '07de8873',
     'phabricator-draggable-list' => 'a16ec1c6',
     'phabricator-fatal-config-template-css' => '8e6c6fcd',
     'phabricator-feed-css' => 'ecd4ec57',
     'phabricator-file-upload' => '477359c8',
     'phabricator-filetree-view-css' => 'fccf9f82',
     'phabricator-flag-css' => '5337623f',
     'phabricator-hovercard' => '14ac66f5',
     'phabricator-hovercard-view-css' => '0d665853',
     'phabricator-keyboard-shortcut' => '1ae869f2',
     'phabricator-keyboard-shortcut-manager' => 'c1700f6f',
     'phabricator-main-menu-view' => '5e8c1ab7',
     'phabricator-nav-view-css' => '0ecd30a1',
     'phabricator-notification' => '0c6946e7',
     'phabricator-notification-css' => '9c279160',
     'phabricator-notification-menu-css' => '713df25a',
     'phabricator-object-selector-css' => '029a133d',
     'phabricator-phtize' => 'd254d646',
     'phabricator-prefab' => '6920d200',
     'phabricator-remarkup-css' => '13368efd',
     'phabricator-search-results-css' => 'ce897fb9',
     'phabricator-shaped-request' => '7cbe244b',
     'phabricator-side-menu-view-css' => '4f2cd343',
     'phabricator-slowvote-css' => '266df6a1',
     'phabricator-source-code-view-css' => '098e9b75',
     'phabricator-standard-page-view' => '43045fb4',
     'phabricator-textareautils' => '5c93c52c',
     'phabricator-title' => 'df5e11d2',
     'phabricator-tooltip' => '1d298e3a',
     'phabricator-ui-example-css' => '528b19de',
     'phabricator-uiexample-javelin-view' => 'd4a14807',
     'phabricator-uiexample-reactor-button' => 'd19198c8',
     'phabricator-uiexample-reactor-checkbox' => '519705ea',
     'phabricator-uiexample-reactor-focus' => '40a6a403',
     'phabricator-uiexample-reactor-input' => '886fd850',
     'phabricator-uiexample-reactor-mouseover' => '47c794d8',
     'phabricator-uiexample-reactor-radio' => '988040b4',
     'phabricator-uiexample-reactor-select' => 'a155550f',
     'phabricator-uiexample-reactor-sendclass' => '1def2711',
     'phabricator-uiexample-reactor-sendproperties' => 'b1f0ccee',
     'phabricator-zindex-css' => '63689f49',
     'phame-css' => '88bd4705',
     'pholio-css' => '95174bdd',
     'pholio-edit-css' => '3ad9d1ee',
     'pholio-inline-comments-css' => '8e545e49',
     'phortune-credit-card-form' => '2290aeef',
     'phortune-credit-card-form-css' => '8391eb02',
     'phortune-css' => '9149f103',
     'phrequent-css' => 'ffc185ad',
     'phriction-document-css' => 'd1861e06',
     'phui-action-panel-css' => '3ee9afd5',
     'phui-box-css' => 'a5bb366d',
     'phui-button-css' => '5ccb5ce4',
     'phui-calendar-css' => 'ccabe893',
     'phui-calendar-day-css' => 'd1cf6f93',
     'phui-calendar-list-css' => 'c1c7f338',
     'phui-calendar-month-css' => '476be7e0',
     'phui-crumbs-view-css' => 'ce840ec2',
     'phui-document-view-css' => '7f67a837',
     'phui-feed-story-css' => '153a2ebf',
     'phui-font-icon-base-css' => '3dad2ae3',
     'phui-fontkit-css' => 'ce1ce3ca',
     'phui-form-css' => '1ecbc461',
     'phui-form-view-css' => 'a0e8f168',
     'phui-header-view-css' => '44fc449c',
     'phui-icon-view-css' => '88ba9081',
     'phui-image-mask-css' => '5a8b09c8',
     'phui-info-panel-css' => '27ea50a1',
     'phui-info-view-css' => '33e54618',
     'phui-inline-comment-view-css' => 'e60ad106',
     'phui-list-view-css' => 'e448b6ba',
     'phui-object-box-css' => '4fd75233',
     'phui-object-item-list-view-css' => 'bf7463f7',
     'phui-pager-css' => 'bea33d23',
     'phui-pinboard-view-css' => '2495140e',
     'phui-property-list-view-css' => '1baf23eb',
     'phui-remarkup-preview-css' => '19ad512b',
     'phui-spacing-css' => '042804d6',
     'phui-status-list-view-css' => '888cedb8',
     'phui-tag-view-css' => '402691cc',
     'phui-text-css' => 'cf019f54',
     'phui-theme-css' => '1ccdcc84',
     'phui-timeline-view-css' => '07a1bd0f',
     'phui-workboard-view-css' => '0cac51a4',
     'phui-workpanel-view-css' => '4bdc2562',
     'phuix-action-list-view' => 'b5c256b8',
     'phuix-action-view' => '8cf6d262',
     'phuix-dropdown-menu' => 'bd4c8dca',
     'policy-css' => '957ea14c',
     'policy-edit-css' => '815c66f7',
     'policy-transaction-detail-css' => '82100a43',
     'ponder-comment-table-css' => '6cdccea7',
     'ponder-feed-view-css' => 'e62615b6',
     'ponder-post-css' => '9d415218',
     'ponder-vote-css' => '8ed6ed8b',
     'project-icon-css' => 'c2ecb7f1',
     'raphael-core' => '51ee6b43',
     'raphael-g' => '40dde778',
     'raphael-g-line' => '40da039e',
     'releeph-core' => '9b3c5733',
     'releeph-preview-branch' => 'b7a6f4a5',
     'releeph-request-differential-create-dialog' => '8d8b92cd',
     'releeph-request-typeahead-css' => '667a48ae',
     'setup-issue-css' => '631c4e92',
     'sprite-login-css' => 'a3526809',
     'sprite-main-header-css' => '37e05e50',
     'sprite-menu-css' => 'fba663c3',
     'sprite-projects-css' => 'b0d9e24f',
     'sprite-tokens-css' => '1706b943',
     'syntax-highlighting-css' => '9fd11da8',
     'tokens-css' => '3d0f239e',
     'typeahead-browse-css' => 'd8581d2c',
     'unhandled-exception-css' => '4c96257a',
   ),
   'requires' => array(
     '01774ab2' => array(
       'javelin-dom',
       'javelin-util',
       'javelin-stratcom',
       'javelin-install',
       'javelin-aphlict',
       'javelin-workflow',
       'javelin-router',
       'javelin-behavior-device',
       'javelin-vector',
     ),
     '029a133d' => array(
       'aphront-dialog-view-css',
     ),
     '031cee25' => array(
       'javelin-behavior',
       'javelin-request',
       'javelin-stratcom',
       'javelin-vector',
       'javelin-dom',
       'javelin-uri',
       'javelin-behavior-device',
       'phabricator-title',
     ),
     '037b59eb' => array(
       'javelin-behavior',
       'javelin-stratcom',
       'javelin-dom',
       'javelin-util',
       'javelin-vector',
       'differential-inline-comment-editor',
     ),
     '048330fa' => array(
       'javelin-behavior',
       'javelin-typeahead-ondemand-source',
       'javelin-typeahead',
       'javelin-dom',
       'javelin-uri',
       'javelin-util',
       'javelin-stratcom',
       'phabricator-prefab',
     ),
     '04875312' => array(
       'aphront-typeahead-control-css',
       'phui-tag-view-css',
     ),
     '05270951' => array(
       'javelin-util',
       'javelin-magical-init',
     ),
     '065227cc' => array(
       'javelin-behavior',
       'javelin-dom',
       'javelin-stratcom',
       'javelin-workflow',
     ),
     '07de8873' => array(
       'javelin-install',
       'javelin-util',
       'javelin-request',
       'javelin-dom',
       'javelin-uri',
       'phabricator-file-upload',
     ),
     '087e919c' => array(
       'javelin-install',
       'javelin-dom',
       'javelin-stratcom',
       'javelin-vector',
     ),
     '095ed313' => array(
       'javelin-behavior',
       'javelin-stratcom',
       'javelin-dom',
       'phabricator-phtize',
       'phabricator-textareautils',
       'javelin-workflow',
       'javelin-vector',
     ),
     '0a3f3021' => array(
       'javelin-behavior',
       'javelin-stratcom',
       'javelin-workflow',
       'javelin-dom',
       'javelin-router',
     ),
     '0c6946e7' => array(
       'javelin-install',
       'javelin-dom',
       'javelin-stratcom',
       'javelin-util',
       'phabricator-notification-css',
     ),
     '0f764c35' => array(
       'javelin-install',
       'javelin-util',
     ),
     '13c739ea' => array(
       'javelin-behavior',
       'javelin-stratcom',
       'javelin-workflow',
       'javelin-dom',
       'javelin-uri',
       'phabricator-textareautils',
     ),
     '147805fa' => array(
       'javelin-magical-init',
       'javelin-install',
       'javelin-util',
       'javelin-vector',
       'javelin-stratcom',
     ),
     '1499a8cb' => array(
       'javelin-behavior',
       'javelin-stratcom',
       'javelin-dom',
       'javelin-history',
     ),
     '14ac66f5' => array(
       'javelin-install',
       'javelin-dom',
       'javelin-vector',
       'javelin-request',
       'javelin-uri',
     ),
     '1ad0a787' => array(
       'javelin-install',
       'javelin-reactor',
       'javelin-util',
       'javelin-reactor-node-calmer',
     ),
     '1ae869f2' => array(
       'javelin-install',
       'javelin-util',
       'phabricator-keyboard-shortcut-manager',
     ),
     '1d298e3a' => array(
       'javelin-install',
       'javelin-util',
       'javelin-dom',
       'javelin-vector',
     ),
     '1def2711' => array(
       'javelin-install',
       'javelin-dom',
       'javelin-reactor-dom',
     ),
+    '1f520937' => array(
+      'phui-fontkit-css',
+    ),
     '2035b9cb' => array(
       'javelin-behavior',
       'javelin-dom',
       'javelin-util',
       'javelin-stratcom',
       'javelin-workflow',
       'phuix-dropdown-menu',
       'phuix-action-list-view',
       'phuix-action-view',
       'phabricator-phtize',
       'changeset-view-manager',
     ),
     '21ba5861' => array(
       'javelin-behavior',
       'javelin-dom',
       'javelin-util',
       'javelin-workflow',
       'javelin-stratcom',
       'conpherence-thread-manager',
     ),
     '2290aeef' => array(
       'javelin-install',
       'javelin-dom',
       'javelin-json',
       'javelin-workflow',
       'javelin-util',
     ),
     '246dc085' => array(
       'javelin-behavior',
       'javelin-stratcom',
       'javelin-dom',
       'javelin-workflow',
       'javelin-quicksand',
       'phabricator-phtize',
       'phabricator-drag-and-drop-file-upload',
       'phabricator-draggable-list',
     ),
-    '271ffdd7' => array(
-      'multirow-row-manager',
-      'javelin-install',
-      'javelin-util',
-      'javelin-dom',
-      'javelin-stratcom',
-      'javelin-json',
-      'phabricator-prefab',
-    ),
     '2818f5ce' => array(
       'javelin-install',
       'javelin-util',
       'javelin-dom',
       'javelin-typeahead-normalizer',
     ),
     '2926fff2' => array(
       'javelin-behavior',
       'javelin-dom',
     ),
     '29274e2b' => array(
       'javelin-install',
       'javelin-util',
     ),
     '2b228192' => array(
       'javelin-behavior',
       'javelin-dom',
       'javelin-util',
       'javelin-workflow',
       'javelin-json',
     ),
     '2b8de964' => array(
       'javelin-install',
       'javelin-util',
     ),
     '2bfa2836' => array(
       'javelin-behavior',
       'javelin-stratcom',
       'javelin-dom',
     ),
     '2c426492' => array(
       'javelin-behavior',
       'javelin-dom',
       'javelin-stratcom',
       'phabricator-keyboard-shortcut',
     ),
     '2caa8fb8' => array(
       'javelin-install',
       'javelin-event',
     ),
     '331b1611' => array(
       'javelin-install',
     ),
     '3ab51e2c' => array(
       'javelin-behavior',
       'javelin-behavior-device',
       'javelin-stratcom',
       'javelin-vector',
       'javelin-dom',
       'javelin-magical-init',
     ),
     '3cb0b2fc' => array(
       'javelin-behavior',
       'javelin-dom',
       'javelin-stratcom',
       'javelin-workflow',
       'javelin-util',
       'javelin-uri',
     ),
     '3ee3408b' => array(
       'javelin-behavior',
       'javelin-behavior-device',
       'javelin-stratcom',
       'phabricator-tooltip',
     ),
     '3f5d6dbf' => array(
       'javelin-behavior',
       'javelin-dom',
       'phortune-credit-card-form',
     ),
     '40a6a403' => array(
       'javelin-install',
       'javelin-dom',
       'javelin-reactor-dom',
     ),
     42126667 => array(
       'javelin-behavior',
       'javelin-dom',
       'javelin-request',
     ),
     '44168bad' => array(
       'javelin-behavior',
       'javelin-dom',
       'phabricator-prefab',
     ),
     '44959b73' => array(
       'javelin-util',
       'javelin-uri',
       'javelin-install',
     ),
     '453c5375' => array(
       'javelin-behavior',
       'javelin-dom',
     ),
     '469c0d9e' => array(
       'javelin-behavior',
       'javelin-dom',
       'javelin-workflow',
     ),
     '477359c8' => array(
       'javelin-install',
       'javelin-dom',
       'phabricator-notification',
     ),
     47830651 => array(
       'javelin-behavior',
       'javelin-dom',
       'javelin-view-renderer',
       'javelin-install',
     ),
     '47c794d8' => array(
       'javelin-install',
       'javelin-dom',
       'javelin-reactor-dom',
     ),
     48086888 => array(
       'javelin-behavior',
       'javelin-dom',
       'javelin-workflow',
     ),
     '49b73b36' => array(
       'javelin-behavior',
       'javelin-dom',
       'javelin-request',
       'javelin-util',
     ),
     '4c95d29e' => array(
       'javelin-behavior',
       'javelin-dom',
       'javelin-util',
       'javelin-json',
       'javelin-stratcom',
       'phabricator-shaped-request',
     ),
     '4cebc641' => array(
       'javelin-install',
     ),
     '4e3e79a6' => array(
       'javelin-behavior',
       'javelin-stratcom',
       'javelin-dom',
     ),
     '4e9b766b' => array(
       'javelin-behavior',
       'javelin-dom',
       'javelin-util',
       'javelin-stratcom',
       'javelin-request',
     ),
     '4fdb476d' => array(
       'javelin-behavior',
       'javelin-stratcom',
       'javelin-dom',
     ),
     '503e17fd' => array(
       'javelin-install',
       'javelin-typeahead-source',
       'javelin-util',
     ),
     '510b5809' => array(
       'javelin-behavior',
       'javelin-util',
       'javelin-dom',
       'javelin-stratcom',
       'javelin-vector',
     ),
     '519705ea' => array(
       'javelin-install',
       'javelin-dom',
       'javelin-reactor-dom',
     ),
     '5359e785' => array(
       'javelin-install',
       'javelin-util',
       'javelin-websocket',
       'javelin-leader',
       'javelin-json',
     ),
     54733475 => array(
       'javelin-behavior',
       'javelin-stratcom',
       'javelin-dom',
       'phuix-dropdown-menu',
     ),
     '54b612ba' => array(
       'javelin-color',
       'javelin-install',
       'javelin-util',
     ),
     '54f314a0' => array(
       'javelin-install',
       'javelin-util',
       'javelin-request',
       'javelin-typeahead-source',
     ),
     '558829c2' => array(
       'javelin-stratcom',
       'javelin-behavior',
       'javelin-vector',
       'javelin-dom',
     ),
     '56a1ca03' => array(
       'javelin-behavior',
       'javelin-behavior-device',
       'javelin-stratcom',
       'javelin-dom',
       'javelin-magical-init',
       'javelin-vector',
       'javelin-request',
       'javelin-util',
     ),
     58562350 => array(
       'javelin-dom',
       'javelin-util',
       'javelin-stratcom',
       'javelin-install',
       'javelin-workflow',
       'javelin-router',
       'javelin-behavior-device',
       'javelin-vector',
     ),
     '59a7976a' => array(
       'javelin-install',
       'javelin-dom',
       'javelin-fx',
     ),
     '59b251eb' => array(
       'javelin-behavior',
       'javelin-stratcom',
       'javelin-vector',
       'javelin-dom',
     ),
     '5b2e3e2b' => array(
       'javelin-stratcom',
       'javelin-request',
       'javelin-dom',
       'javelin-vector',
       'javelin-install',
       'javelin-util',
       'javelin-mask',
       'javelin-uri',
       'javelin-routable',
     ),
     '5c54cbf3' => array(
       'javelin-behavior',
       'javelin-stratcom',
       'javelin-dom',
     ),
     '5c93c52c' => array(
       'javelin-install',
       'javelin-dom',
       'javelin-vector',
     ),
     '5d7c9f33' => array(
       'javelin-behavior',
       'javelin-stratcom',
       'javelin-dom',
     ),
     '5e8c1ab7' => array(
       'phui-theme-css',
     ),
     '5e9f347c' => array(
       'javelin-behavior',
       'multirow-row-manager',
       'javelin-dom',
       'javelin-util',
       'phabricator-prefab',
       'javelin-json',
     ),
     '5f05d817' => array(
       'phui-fontkit-css',
     ),
     '5fefb143' => array(
       'javelin-behavior',
       'javelin-dom',
       'javelin-workflow',
       'javelin-stratcom',
     ),
     60479091 => array(
       'phabricator-busy',
       'javelin-behavior',
     ),
     '60821bc7' => array(
       'javelin-behavior',
       'javelin-stratcom',
       'javelin-dom',
     ),
     '6153c708' => array(
       'javelin-behavior',
       'javelin-stratcom',
       'javelin-dom',
       'javelin-workflow',
     ),
     '61cbc29a' => array(
       'javelin-magical-init',
       'javelin-util',
     ),
     '62dfea03' => array(
       'javelin-install',
       'javelin-util',
     ),
     '635de1ec' => array(
       'javelin-behavior',
       'javelin-stratcom',
       'javelin-workflow',
       'javelin-dom',
     ),
     '6882e80a' => array(
       'javelin-dom',
     ),
     '6920d200' => array(
       'javelin-install',
       'javelin-util',
       'javelin-dom',
       'javelin-typeahead',
       'javelin-tokenizer',
       'javelin-typeahead-preloaded-source',
       'javelin-typeahead-ondemand-source',
       'javelin-dom',
       'javelin-stratcom',
       'javelin-util',
     ),
     '69adf288' => array(
       'javelin-install',
     ),
     '6c0e62fa' => array(
       'javelin-install',
       'javelin-typeahead-source',
     ),
     '6c2b09a2' => array(
       'javelin-install',
       'javelin-util',
     ),
     '6c53634d' => array(
       'javelin-install',
       'javelin-event',
       'javelin-util',
       'javelin-magical-init',
     ),
     '6d3e1947' => array(
       'javelin-behavior',
       'javelin-diffusion-locate-file-source',
       'javelin-dom',
       'javelin-typeahead',
       'javelin-uri',
     ),
     '6d49590e' => array(
       'javelin-behavior',
       'javelin-dom',
       'phabricator-drag-and-drop-file-upload',
       'phabricator-textareautils',
     ),
     '6eff08aa' => array(
       'javelin-install',
       'javelin-util',
       'javelin-stratcom',
     ),
     '70baed2f' => array(
       'javelin-install',
       'javelin-dom',
       'javelin-vector',
       'javelin-util',
     ),
     '7319e029' => array(
       'javelin-behavior',
       'javelin-dom',
     ),
     '73712bee' => array(
       'phui-inline-comment-view-css',
     ),
     '73d09eef' => array(
       'javelin-behavior',
       'javelin-vector',
       'javelin-dom',
     ),
     '76b9fc3e' => array(
       'javelin-behavior',
       'javelin-stratcom',
       'javelin-workflow',
       'javelin-dom',
       'phabricator-draggable-list',
     ),
     '76f4ebed' => array(
       'javelin-install',
       'javelin-reactor',
       'javelin-util',
     ),
     '7814b593' => array(
       'javelin-request',
       'javelin-behavior',
       'javelin-dom',
       'javelin-router',
       'javelin-util',
       'phabricator-busy',
     ),
+    '782ab6e7' => array(
+      'javelin-behavior',
+      'javelin-dom',
+      'javelin-util',
+      'phabricator-prefab',
+      'multirow-row-manager',
+      'javelin-json',
+    ),
     '7927a7d3' => array(
       'javelin-behavior',
       'javelin-quicksand',
     ),
     '7a68dda3' => array(
       'owners-path-editor',
       'javelin-behavior',
     ),
-    '7a85ea13' => array(
-      'phui-fontkit-css',
-    ),
     '7b98d7c5' => array(
       'javelin-behavior',
       'javelin-dom',
       'javelin-stratcom',
       'javelin-util',
     ),
     '7cbe244b' => array(
       'javelin-install',
       'javelin-util',
       'javelin-request',
       'javelin-router',
     ),
+    '7d470398' => array(
+      'javelin-behavior',
+      'javelin-dom',
+      'javelin-util',
+      'phuix-dropdown-menu',
+      'phuix-action-list-view',
+      'phuix-action-view',
+      'javelin-workflow',
+    ),
     '7e41274a' => array(
       'javelin-install',
     ),
     '7ebaeed3' => array(
       'herald-rule-editor',
       'javelin-behavior',
     ),
     '7ee2b591' => array(
       'javelin-behavior',
       'javelin-history',
     ),
     82439934 => array(
       'javelin-behavior',
       'javelin-dom',
       'javelin-util',
       'javelin-stratcom',
       'javelin-workflow',
       'phabricator-draggable-list',
     ),
     '834a1173' => array(
       'javelin-behavior',
       'javelin-scrollbar',
     ),
     '84845b5b' => array(
       'javelin-behavior',
       'javelin-dom',
       'javelin-stratcom',
       'javelin-workflow',
       'phabricator-draggable-list',
     ),
     '85ea0626' => array(
       'javelin-install',
     ),
     '8694b1df' => array(
       'javelin-behavior',
       'javelin-dom',
       'javelin-stratcom',
       'phabricator-tooltip',
       'changeset-view-manager',
     ),
     '88236f00' => array(
       'javelin-behavior',
       'phabricator-keyboard-shortcut',
       'javelin-stratcom',
     ),
     '886fd850' => array(
       'javelin-install',
       'javelin-reactor-dom',
       'javelin-view-html',
       'javelin-view-interpreter',
       'javelin-view-renderer',
     ),
     '887ad43f' => array(
       'javelin-behavior',
       'javelin-request',
       'javelin-stratcom',
       'javelin-dom',
     ),
     '88f0c5b3' => array(
       'javelin-behavior',
       'javelin-dom',
       'javelin-vector',
     ),
     '8a41885b' => array(
       'javelin-install',
       'javelin-dom',
     ),
     '8b3fd187' => array(
       'javelin-install',
       'javelin-util',
       'javelin-request',
       'javelin-typeahead-source',
     ),
     '8bfbb401' => array(
       'javelin-behavior',
       'javelin-util',
       'javelin-dom',
       'javelin-stratcom',
       'javelin-vector',
       'javelin-typeahead-static-source',
     ),
     '8ce821c5' => array(
       'phabricator-notification',
       'javelin-stratcom',
       'javelin-behavior',
     ),
     '8cf6d262' => array(
       'javelin-install',
       'javelin-dom',
       'javelin-util',
     ),
     '8ef9ab58' => array(
       'javelin-behavior',
       'javelin-dom',
       'javelin-stratcom',
     ),
     '9007c197' => array(
       'javelin-behavior',
       'javelin-dom',
       'javelin-stratcom',
     ),
     93568464 => array(
       'javelin-behavior',
       'javelin-dom',
       'javelin-stratcom',
       'javelin-workflow',
       'javelin-util',
       'phabricator-notification',
       'javelin-behavior-device',
       'phuix-dropdown-menu',
       'phuix-action-list-view',
       'phuix-action-view',
       'conpherence-thread-manager',
     ),
     '93d0c9e3' => array(
       'javelin-behavior',
       'javelin-stratcom',
       'javelin-workflow',
       'javelin-dom',
     ),
     '9414ff18' => array(
       'javelin-behavior',
       'javelin-resource',
       'javelin-stratcom',
       'javelin-workflow',
       'javelin-util',
     ),
     '949c0fe5' => array(
       'javelin-install',
     ),
     '94b750d2' => array(
       'javelin-install',
       'javelin-stratcom',
       'javelin-util',
       'javelin-behavior',
       'javelin-json',
       'javelin-dom',
       'javelin-resource',
       'javelin-routable',
     ),
     '988040b4' => array(
       'javelin-install',
       'javelin-dom',
       'javelin-reactor-dom',
     ),
-    '9a340b3d' => array(
-      'javelin-behavior',
-      'javelin-dom',
-      'javelin-util',
-      'phuix-dropdown-menu',
-      'phuix-action-list-view',
-      'phuix-action-view',
-      'javelin-workflow',
-    ),
     '9f36c42d' => array(
       'javelin-behavior',
       'javelin-stratcom',
       'javelin-dom',
       'javelin-vector',
     ),
     'a0b57eb8' => array(
       'javelin-behavior',
       'javelin-dom',
       'javelin-stratcom',
       'javelin-workflow',
       'javelin-util',
       'phabricator-keyboard-shortcut',
     ),
     'a155550f' => array(
       'javelin-install',
       'javelin-dom',
       'javelin-reactor-dom',
     ),
     'a16ec1c6' => array(
       'javelin-install',
       'javelin-dom',
       'javelin-stratcom',
       'javelin-util',
       'javelin-vector',
       'javelin-magical-init',
     ),
     'a205cf28' => array(
       'javelin-behavior',
       'javelin-stratcom',
       'javelin-dom',
       'javelin-vector',
       'javelin-install',
     ),
     'a464fe03' => array(
       'javelin-behavior',
       'javelin-uri',
       'phabricator-notification',
     ),
     'a80d0378' => array(
       'javelin-behavior',
       'javelin-stratcom',
       'javelin-dom',
     ),
     'a8d8459d' => array(
       'javelin-behavior',
       'javelin-dom',
       'javelin-stratcom',
     ),
     'a8da01f0' => array(
       'javelin-behavior',
       'javelin-uri',
       'phabricator-keyboard-shortcut',
     ),
     'a9f88de2' => array(
       'javelin-behavior',
       'javelin-dom',
       'javelin-stratcom',
       'javelin-workflow',
       'javelin-fx',
       'javelin-util',
     ),
     'aa1733d0' => array(
       'multirow-row-manager',
       'javelin-install',
       'path-typeahead',
       'javelin-dom',
       'javelin-util',
       'phabricator-prefab',
     ),
     'ab5f468d' => array(
       'javelin-dom',
       'javelin-util',
       'javelin-stratcom',
       'javelin-install',
     ),
     'b064af76' => array(
       'javelin-behavior',
       'javelin-stratcom',
       'javelin-dom',
       'javelin-request',
       'javelin-util',
       'phabricator-shaped-request',
     ),
     'b1a59974' => array(
       'javelin-behavior',
       'javelin-aphlict',
       'javelin-stratcom',
       'javelin-request',
       'javelin-uri',
       'javelin-dom',
       'javelin-json',
       'javelin-router',
       'javelin-util',
       'javelin-leader',
       'javelin-sound',
       'phabricator-notification',
     ),
     'b1f0ccee' => array(
       'javelin-install',
       'javelin-dom',
       'javelin-reactor-dom',
     ),
     'b23b49e6' => array(
       'javelin-behavior',
       'javelin-dom',
       'javelin-util',
       'javelin-request',
       'phabricator-shaped-request',
     ),
     'b2b4fbaf' => array(
       'javelin-behavior',
       'javelin-dom',
       'javelin-uri',
       'javelin-request',
     ),
+    'b2cae298' => array(
+      'multirow-row-manager',
+      'javelin-install',
+      'javelin-util',
+      'javelin-dom',
+      'javelin-stratcom',
+      'javelin-json',
+      'phabricator-prefab',
+    ),
     'b3a4b884' => array(
       'javelin-behavior',
       'phabricator-prefab',
     ),
     'b3e7d692' => array(
       'javelin-install',
     ),
     'b42eddc7' => array(
       'javelin-install',
       'javelin-dom',
       'javelin-typeahead-preloaded-source',
       'javelin-util',
     ),
     'b5c256b8' => array(
       'javelin-install',
       'javelin-dom',
     ),
     'b5d57730' => array(
       'javelin-install',
       'javelin-stratcom',
       'javelin-dom',
       'javelin-util',
     ),
     'b6993408' => array(
       'javelin-behavior',
       'javelin-stratcom',
       'javelin-dom',
       'javelin-json',
       'phabricator-draggable-list',
     ),
     'ba4fa35c' => array(
       'javelin-behavior',
       'javelin-dom',
       'javelin-util',
       'javelin-vector',
       'javelin-stratcom',
       'javelin-workflow',
       'phabricator-draggable-list',
     ),
     'bba9eedf' => array(
       'javelin-behavior',
       'javelin-stratcom',
       'javelin-dom',
     ),
     'bd4c8dca' => array(
       'javelin-install',
       'javelin-util',
       'javelin-dom',
       'javelin-vector',
       'javelin-stratcom',
     ),
     'bdaf4d04' => array(
       'javelin-behavior',
       'javelin-dom',
       'javelin-util',
       'javelin-request',
     ),
     'be807912' => array(
       'javelin-behavior',
       'javelin-dom',
       'javelin-util',
       'phabricator-shaped-request',
     ),
     'bea81850' => array(
       'javelin-behavior',
       'javelin-dom',
       'javelin-stratcom',
       'javelin-uri',
     ),
     'c1700f6f' => array(
       'javelin-install',
       'javelin-util',
       'javelin-stratcom',
       'javelin-dom',
       'javelin-vector',
     ),
     'c72aa091' => array(
       'javelin-behavior',
       'javelin-dom',
       'javelin-stratcom',
       'javelin-behavior-device',
       'javelin-scrollbar',
       'javelin-quicksand',
       'phabricator-keyboard-shortcut',
       'conpherence-thread-manager',
     ),
     'c8e57404' => array(
       'javelin-behavior',
       'javelin-dom',
       'javelin-uri',
       'javelin-mask',
       'phabricator-drag-and-drop-file-upload',
     ),
     'c90a04fc' => array(
       'javelin-dom',
       'javelin-dynval',
       'javelin-reactor',
       'javelin-reactornode',
       'javelin-install',
       'javelin-util',
     ),
     'ca3f91eb' => array(
       'javelin-behavior',
       'javelin-dom',
       'javelin-stratcom',
       'phabricator-phtize',
     ),
     'cf86d16a' => array(
       'javelin-behavior',
       'javelin-dom',
       'javelin-workflow',
       'phabricator-drag-and-drop-file-upload',
     ),
     'd19198c8' => array(
       'javelin-install',
       'javelin-dom',
       'javelin-util',
       'javelin-dynval',
       'javelin-reactor-dom',
     ),
     'd254d646' => array(
       'javelin-util',
     ),
     'd3782c93' => array(
       'javelin-behavior',
       'javelin-dom',
       'javelin-util',
       'javelin-stratcom',
       'javelin-workflow',
       'javelin-behavior-device',
       'javelin-history',
       'javelin-vector',
       'javelin-scrollbar',
       'phabricator-title',
       'phabricator-shaped-request',
       'conpherence-thread-manager',
     ),
     'd4505101' => array(
       'javelin-stratcom',
       'javelin-install',
       'javelin-uri',
       'javelin-util',
     ),
     'd4a14807' => array(
       'javelin-install',
       'javelin-dom',
       'javelin-view',
     ),
     'd4c87bf4' => array(
       'javelin-dom',
       'javelin-util',
       'javelin-stratcom',
       'javelin-install',
       'javelin-request',
       'javelin-workflow',
     ),
     'd4eecc63' => array(
       'javelin-behavior',
       'javelin-dom',
       'javelin-stratcom',
     ),
     'd75709e6' => array(
       'javelin-behavior',
       'javelin-workflow',
       'javelin-json',
       'javelin-dom',
       'phabricator-keyboard-shortcut',
     ),
     'd835b03a' => array(
       'javelin-behavior',
       'javelin-dom',
       'javelin-util',
       'phabricator-shaped-request',
     ),
     'dbbf48b6' => array(
       'javelin-behavior',
       'javelin-stratcom',
       'javelin-dom',
       'phabricator-busy',
     ),
     'de2e896f' => array(
       'javelin-behavior',
       'javelin-dom',
       'javelin-typeahead',
       'javelin-typeahead-ondemand-source',
       'javelin-dom',
     ),
     'df5e11d2' => array(
       'javelin-install',
     ),
     'e10f8e18' => array(
       'javelin-behavior',
       'javelin-dom',
       'phabricator-prefab',
     ),
     'e1d25dfb' => array(
       'javelin-behavior',
       'javelin-stratcom',
       'javelin-workflow',
       'javelin-dom',
       'phabricator-draggable-list',
     ),
     'e1ff79b1' => array(
       'javelin-behavior',
       'javelin-stratcom',
       'javelin-dom',
     ),
     'e292eaf4' => array(
       'javelin-install',
     ),
     'e379b58e' => array(
       'javelin-behavior',
       'javelin-stratcom',
       'javelin-vector',
       'javelin-dom',
       'javelin-uri',
     ),
     'e4cc26b3' => array(
       'javelin-behavior',
       'javelin-dom',
     ),
     'e5822781' => array(
       'javelin-behavior',
       'javelin-dom',
       'javelin-json',
       'javelin-workflow',
       'javelin-magical-init',
     ),
     'e6e25838' => array(
       'javelin-install',
     ),
     'e9581f08' => array(
       'javelin-behavior',
       'javelin-stratcom',
       'javelin-workflow',
       'javelin-dom',
       'phabricator-draggable-list',
     ),
     'ea681761' => array(
       'javelin-behavior',
       'javelin-aphlict',
       'phabricator-phtize',
       'javelin-dom',
     ),
     'efe49472' => array(
       'javelin-install',
       'javelin-util',
     ),
     'f36e01af' => array(
       'javelin-behavior',
       'javelin-behavior-device',
       'javelin-stratcom',
       'javelin-vector',
       'phabricator-hovercard',
     ),
     'f411b6ae' => array(
       'javelin-behavior',
       'javelin-stratcom',
       'javelin-util',
       'javelin-dom',
       'javelin-request',
       'phabricator-keyboard-shortcut',
     ),
-    'f5d1233b' => array(
-      'javelin-behavior',
-      'javelin-dom',
-      'javelin-util',
-      'phabricator-prefab',
-      'multirow-row-manager',
-      'javelin-json',
-    ),
     'f6555212' => array(
       'javelin-install',
       'javelin-reactornode',
       'javelin-util',
       'javelin-reactor',
     ),
     'f7379f45' => array(
       'javelin-behavior',
       'javelin-dom',
       'javelin-util',
       'phabricator-shaped-request',
     ),
     'f7fc67ec' => array(
       'javelin-install',
       'javelin-typeahead',
       'javelin-dom',
       'javelin-request',
       'javelin-typeahead-ondemand-source',
       'javelin-util',
     ),
     'f829edb3' => array(
       'javelin-view',
       'javelin-install',
       'javelin-dom',
     ),
     'f8ba29d7' => array(
       'javelin-behavior',
       'javelin-stratcom',
       'javelin-dom',
       'javelin-mask',
       'javelin-util',
       'phabricator-busy',
     ),
     'fa0f4fc2' => array(
       'javelin-behavior',
       'javelin-dom',
       'javelin-vector',
       'javelin-magical-init',
     ),
     'fbe497e7' => array(
       'javelin-behavior',
       'javelin-util',
       'javelin-stratcom',
       'javelin-dom',
       'javelin-vector',
       'javelin-magical-init',
       'javelin-request',
       'javelin-history',
       'javelin-workflow',
       'javelin-mask',
       'javelin-behavior-device',
       'phabricator-keyboard-shortcut',
     ),
     'fc91ab6c' => array(
       'javelin-behavior',
       'javelin-dom',
       'phortune-credit-card-form',
     ),
     'fe287620' => array(
       'javelin-install',
       'javelin-dom',
       'javelin-view-visitor',
       'javelin-util',
     ),
   ),
   'packages' => array(
     'core.pkg.css' => array(
       'phabricator-core-css',
       'phabricator-zindex-css',
       'phui-button-css',
       'phabricator-standard-page-view',
       'aphront-dialog-view-css',
       'phui-form-view-css',
       'aphront-panel-view-css',
       'aphront-table-view-css',
       'aphront-tokenizer-control-css',
       'aphront-typeahead-control-css',
       'aphront-list-filter-view-css',
       'phabricator-remarkup-css',
       'syntax-highlighting-css',
       'phui-pager-css',
       'aphront-tooltip-css',
       'phabricator-flag-css',
       'phui-info-view-css',
       'sprite-menu-css',
       'phabricator-main-menu-view',
       'phabricator-notification-css',
       'phabricator-notification-menu-css',
       'lightbox-attachment-css',
       'phui-header-view-css',
       'phabricator-filetree-view-css',
       'phabricator-nav-view-css',
       'phabricator-side-menu-view-css',
       'phui-crumbs-view-css',
       'phui-object-item-list-view-css',
       'global-drag-and-drop-css',
       'phui-spacing-css',
       'phui-form-css',
       'phui-icon-view-css',
       'phabricator-application-launch-view-css',
       'phabricator-action-list-view-css',
       'phui-property-list-view-css',
       'phui-tag-view-css',
       'phui-list-view-css',
       'font-fontawesome',
       'phui-font-icon-base-css',
       'phui-box-css',
       'phui-object-box-css',
       'phui-timeline-view-css',
       'sprite-tokens-css',
       'tokens-css',
       'phui-status-list-view-css',
       'phui-feed-story-css',
       'phabricator-feed-css',
       'phabricator-dashboard-css',
       'aphront-multi-column-view-css',
       'conpherence-durable-column-view',
     ),
     'core.pkg.js' => array(
       'javelin-util',
       'javelin-install',
       'javelin-event',
       'javelin-stratcom',
       'javelin-behavior',
       'javelin-resource',
       'javelin-request',
       'javelin-vector',
       'javelin-dom',
       'javelin-json',
       'javelin-uri',
       'javelin-workflow',
       'javelin-mask',
       'javelin-typeahead',
       'javelin-typeahead-normalizer',
       'javelin-typeahead-source',
       'javelin-typeahead-preloaded-source',
       'javelin-typeahead-ondemand-source',
       'javelin-tokenizer',
       'javelin-history',
       'javelin-router',
       'javelin-routable',
       'javelin-behavior-aphront-basic-tokenizer',
       'javelin-behavior-workflow',
       'javelin-behavior-aphront-form-disable-on-submit',
       'phabricator-keyboard-shortcut-manager',
       'phabricator-keyboard-shortcut',
       'javelin-behavior-phabricator-keyboard-shortcuts',
       'javelin-behavior-refresh-csrf',
       'javelin-behavior-phabricator-watch-anchor',
       'javelin-behavior-phabricator-autofocus',
       'phuix-dropdown-menu',
       'phuix-action-list-view',
       'phuix-action-view',
       'phabricator-phtize',
       'javelin-behavior-phabricator-oncopy',
       'phabricator-tooltip',
       'javelin-behavior-phabricator-tooltips',
       'phabricator-prefab',
       'javelin-behavior-device',
       'javelin-behavior-toggle-class',
       'javelin-behavior-lightbox-attachments',
       'phabricator-busy',
       'javelin-aphlict',
       'phabricator-notification',
       'javelin-behavior-aphlict-listen',
       'javelin-behavior-phabricator-search-typeahead',
       'javelin-behavior-aphlict-dropdown',
       'javelin-behavior-history-install',
       'javelin-behavior-phabricator-gesture',
       'javelin-behavior-phabricator-active-nav',
       'javelin-behavior-phabricator-nav',
       'javelin-behavior-phabricator-remarkup-assist',
       'phabricator-textareautils',
       'phabricator-file-upload',
       'javelin-behavior-global-drag-and-drop',
       'javelin-behavior-phabricator-reveal-content',
       'phabricator-hovercard',
       'javelin-behavior-phabricator-hovercards',
       'javelin-color',
       'javelin-fx',
       'phabricator-draggable-list',
       'javelin-behavior-phabricator-transaction-list',
       'javelin-behavior-phabricator-show-older-transactions',
       'javelin-behavior-phui-dropdown-menu',
       'javelin-behavior-doorkeeper-tag',
       'phabricator-title',
       'javelin-leader',
       'javelin-websocket',
       'javelin-behavior-dashboard-async-panel',
       'javelin-behavior-dashboard-tab-panel',
       'javelin-quicksand',
       'javelin-behavior-quicksand-blacklist',
       'javelin-behavior-high-security-warning',
       'javelin-scrollbar',
       'javelin-behavior-scrollbar',
       'javelin-behavior-durable-column',
       'conpherence-thread-manager',
     ),
     'darkconsole.pkg.js' => array(
       'javelin-behavior-dark-console',
       'javelin-behavior-error-log',
     ),
     'differential.pkg.css' => array(
       'differential-core-view-css',
       'differential-changeset-view-css',
       'differential-results-table-css',
       'differential-revision-history-css',
       'differential-revision-list-css',
       'differential-table-of-contents-css',
       'differential-revision-comment-css',
       'differential-revision-add-comment-css',
       'phabricator-object-selector-css',
       'phabricator-content-source-view-css',
       'inline-comment-summary-css',
       'phui-inline-comment-view-css',
     ),
     'differential.pkg.js' => array(
       'phabricator-drag-and-drop-file-upload',
       'phabricator-shaped-request',
       'javelin-behavior-differential-feedback-preview',
       'javelin-behavior-differential-edit-inline-comments',
       'javelin-behavior-differential-populate',
       'javelin-behavior-differential-diff-radios',
       'javelin-behavior-differential-comment-jump',
       'javelin-behavior-differential-add-reviewers-and-ccs',
       'javelin-behavior-differential-keyboard-navigation',
       'javelin-behavior-aphront-drag-and-drop-textarea',
       'javelin-behavior-phabricator-object-selector',
       'javelin-behavior-repository-crossreference',
       'javelin-behavior-load-blame',
       'differential-inline-comment-editor',
       'javelin-behavior-differential-dropdown-menus',
       'javelin-behavior-differential-toggle-files',
       'javelin-behavior-differential-user-select',
       'javelin-behavior-aphront-more',
       'changeset-view-manager',
     ),
     'diffusion.pkg.css' => array(
       'diffusion-icons-css',
     ),
     'diffusion.pkg.js' => array(
       'javelin-behavior-diffusion-pull-lastmodified',
       'javelin-behavior-diffusion-commit-graph',
       'javelin-behavior-audit-preview',
     ),
     'maniphest.pkg.css' => array(
       'maniphest-task-summary-css',
     ),
     'maniphest.pkg.js' => array(
       'javelin-behavior-maniphest-batch-selector',
       'javelin-behavior-maniphest-transaction-controls',
       'javelin-behavior-maniphest-transaction-preview',
       'javelin-behavior-maniphest-transaction-expand',
       'javelin-behavior-maniphest-subpriority-editor',
       'javelin-behavior-maniphest-list-editor',
     ),
   ),
 );
diff --git a/resources/sql/autopatches/20150609.spaces.1.pholio.sql b/resources/sql/autopatches/20150609.spaces.1.pholio.sql
new file mode 100644
index 000000000..903afb86d
--- /dev/null
+++ b/resources/sql/autopatches/20150609.spaces.1.pholio.sql
@@ -0,0 +1,2 @@
+ALTER TABLE {$NAMESPACE}_pholio.pholio_mock
+  ADD spacePHID VARBINARY(64);
diff --git a/resources/sql/autopatches/20150609.spaces.2.maniphest.sql b/resources/sql/autopatches/20150609.spaces.2.maniphest.sql
new file mode 100644
index 000000000..d46b5e590
--- /dev/null
+++ b/resources/sql/autopatches/20150609.spaces.2.maniphest.sql
@@ -0,0 +1,2 @@
+ALTER TABLE {$NAMESPACE}_maniphest.maniphest_task
+  ADD spacePHID VARBINARY(64);
diff --git a/resources/sql/autopatches/20150610.spaces.1.desc.sql b/resources/sql/autopatches/20150610.spaces.1.desc.sql
new file mode 100644
index 000000000..c62cd8aec
--- /dev/null
+++ b/resources/sql/autopatches/20150610.spaces.1.desc.sql
@@ -0,0 +1,2 @@
+ALTER TABLE {$NAMESPACE}_spaces.spaces_namespace
+  ADD description LONGTEXT COLLATE {$COLLATE_TEXT} NOT NULL;
diff --git a/resources/sql/autopatches/20150610.spaces.2.edge.sql b/resources/sql/autopatches/20150610.spaces.2.edge.sql
new file mode 100644
index 000000000..bc72ed222
--- /dev/null
+++ b/resources/sql/autopatches/20150610.spaces.2.edge.sql
@@ -0,0 +1,16 @@
+CREATE TABLE {$NAMESPACE}_spaces.edge (
+  src VARBINARY(64) NOT NULL,
+  type INT UNSIGNED NOT NULL,
+  dst VARBINARY(64) NOT NULL,
+  dateCreated INT UNSIGNED NOT NULL,
+  seq INT UNSIGNED NOT NULL,
+  dataID INT UNSIGNED,
+  PRIMARY KEY (src, type, dst),
+  KEY `src` (src, type, dateCreated, seq),
+  UNIQUE KEY `key_dst` (dst, type, src)
+) ENGINE=InnoDB, COLLATE {$COLLATE_TEXT};
+
+CREATE TABLE {$NAMESPACE}_spaces.edgedata (
+  id INT UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT,
+  data LONGTEXT NOT NULL COLLATE {$COLLATE_TEXT}
+) ENGINE=InnoDB, COLLATE {$COLLATE_TEXT};
diff --git a/resources/sql/autopatches/20150610.spaces.3.archive.sql b/resources/sql/autopatches/20150610.spaces.3.archive.sql
new file mode 100644
index 000000000..8dd55959a
--- /dev/null
+++ b/resources/sql/autopatches/20150610.spaces.3.archive.sql
@@ -0,0 +1,2 @@
+ALTER TABLE {$NAMESPACE}_spaces.spaces_namespace
+  ADD isArchived BOOL NOT NULL;
diff --git a/resources/sql/autopatches/20150611.spaces.1.mailxaction.sql b/resources/sql/autopatches/20150611.spaces.1.mailxaction.sql
new file mode 100644
index 000000000..fe519f48a
--- /dev/null
+++ b/resources/sql/autopatches/20150611.spaces.1.mailxaction.sql
@@ -0,0 +1,19 @@
+CREATE TABLE {$NAMESPACE}_metamta.metamta_applicationemailtransaction (
+  id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
+  phid VARBINARY(64) NOT NULL,
+  authorPHID VARBINARY(64) NOT NULL,
+  objectPHID VARBINARY(64) NOT NULL,
+  viewPolicy VARBINARY(64) NOT NULL,
+  editPolicy VARBINARY(64) NOT NULL,
+  commentPHID VARBINARY(64) DEFAULT NULL,
+  commentVersion INT UNSIGNED NOT NULL,
+  transactionType VARCHAR(32) COLLATE {$COLLATE_TEXT} NOT NULL,
+  oldValue LONGTEXT COLLATE {$COLLATE_TEXT} NOT NULL,
+  newValue LONGTEXT COLLATE {$COLLATE_TEXT} NOT NULL,
+  contentSource LONGTEXT COLLATE {$COLLATE_TEXT} NOT NULL,
+  metadata LONGTEXT COLLATE {$COLLATE_TEXT} NOT NULL,
+  dateCreated INT UNSIGNED NOT NULL,
+  dateModified INT UNSIGNED NOT NULL,
+  UNIQUE KEY `key_phid` (`phid`),
+  KEY `key_object` (`objectPHID`)
+) ENGINE=InnoDB, COLLATE {$COLLATE_TEXT};
diff --git a/resources/sql/autopatches/20150611.spaces.2.appmail.sql b/resources/sql/autopatches/20150611.spaces.2.appmail.sql
new file mode 100644
index 000000000..c846e4338
--- /dev/null
+++ b/resources/sql/autopatches/20150611.spaces.2.appmail.sql
@@ -0,0 +1,2 @@
+ALTER TABLE {$NAMESPACE}_metamta.metamta_applicationemail
+  ADD spacePHID VARBINARY(64);
diff --git a/resources/sql/patches/20131121.repocredentials.2.mig.php b/resources/sql/patches/20131121.repocredentials.2.mig.php
index 3953b8416..3da813408 100644
--- a/resources/sql/patches/20131121.repocredentials.2.mig.php
+++ b/resources/sql/patches/20131121.repocredentials.2.mig.php
@@ -1,139 +1,139 @@
 <?php
 
 $table = new PhabricatorRepository();
 $conn_w = $table->establishConnection('w');
 $viewer = PhabricatorUser::getOmnipotentUser();
 
 $map = array();
 foreach (new LiskMigrationIterator($table) as $repository) {
   $callsign = $repository->getCallsign();
   echo pht('Examining repository %s...', $callsign)."\n";
 
   if ($repository->getCredentialPHID()) {
     echo pht('...already has a Credential.')."\n";
     continue;
   }
 
   $raw_uri = $repository->getRemoteURI();
   if (!$raw_uri) {
     echo pht('...no remote URI.')."\n";
     continue;
   }
 
   $uri = new PhutilURI($raw_uri);
 
   $proto = strtolower($uri->getProtocol());
   if ($proto == 'http' || $proto == 'https' || $proto == 'svn') {
     $username = $repository->getDetail('http-login');
     $secret = $repository->getDetail('http-pass');
-    $type = PassphraseCredentialTypePassword::CREDENTIAL_TYPE;
+    $type = PassphrasePasswordCredentialType::CREDENTIAL_TYPE;
   } else {
     $username = $repository->getDetail('ssh-login');
     if (!$username) {
       // If there's no explicit username, check for one in the URI. This is
       // possible with older repositories.
       $username = $uri->getUser();
       if (!$username) {
         // Also check for a Git/SCP-style URI.
         $git_uri = new PhutilGitURI($raw_uri);
         $username = $git_uri->getUser();
       }
     }
     $file = $repository->getDetail('ssh-keyfile');
     if ($file) {
       $secret = $file;
-      $type = PassphraseCredentialTypeSSHPrivateKeyFile::CREDENTIAL_TYPE;
+      $type = PassphraseSSHPrivateKeyFileCredentialType::CREDENTIAL_TYPE;
     } else {
       $secret = $repository->getDetail('ssh-key');
-      $type = PassphraseCredentialTypeSSHPrivateKeyText::CREDENTIAL_TYPE;
+      $type = PassphraseSSHPrivateKeyTextCredentialType::CREDENTIAL_TYPE;
     }
   }
 
   if (!$username || !$secret) {
     echo pht('...no credentials set.')."\n";
     continue;
   }
 
   $map[$type][$username][$secret][] = $repository;
   echo pht('...will migrate.')."\n";
 }
 
 $passphrase = new PassphraseSecret();
 $passphrase->openTransaction();
 $table->openTransaction();
 
 foreach ($map as $credential_type => $credential_usernames) {
   $type = PassphraseCredentialType::getTypeByConstant($credential_type);
   foreach ($credential_usernames as $username => $credential_secrets) {
     foreach ($credential_secrets as $secret_plaintext => $repositories) {
       $callsigns = mpull($repositories, 'getCallsign');
 
       $signs = implode(', ', $callsigns);
 
       $name = pht(
         'Migrated Repository Credential (%s)',
         id(new PhutilUTF8StringTruncator())
           ->setMaximumGlyphs(128)
           ->truncateString($signs));
 
       echo pht('Creating: %s...', $name)."\n";
 
       $secret = id(new PassphraseSecret())
         ->setSecretData($secret_plaintext)
         ->save();
 
       $secret_id = $secret->getID();
 
       $credential = PassphraseCredential::initializeNewCredential($viewer)
         ->setCredentialType($type->getCredentialType())
         ->setProvidesType($type->getProvidesType())
         ->setViewPolicy(PhabricatorPolicies::POLICY_ADMIN)
         ->setEditPolicy(PhabricatorPolicies::POLICY_ADMIN)
         ->setName($name)
         ->setUsername($username)
         ->setSecretID($secret_id);
 
       $credential->setPHID($credential->generatePHID());
 
       queryfx(
         $credential->establishConnection('w'),
         'INSERT INTO %T (name, credentialType, providesType, viewPolicy,
           editPolicy, description, username, secretID, isDestroyed,
           phid, dateCreated, dateModified)
           VALUES (%s, %s, %s, %s, %s, %s, %s, %d, %d, %s, %d, %d)',
         $credential->getTableName(),
         $credential->getName(),
         $credential->getCredentialType(),
         $credential->getProvidesType(),
         $credential->getViewPolicy(),
         $credential->getEditPolicy(),
         $credential->getDescription(),
         $credential->getUsername(),
         $credential->getSecretID(),
         $credential->getIsDestroyed(),
         $credential->getPHID(),
         time(),
         time());
 
       foreach ($repositories as $repository) {
         queryfx(
           $conn_w,
           'UPDATE %T SET credentialPHID = %s WHERE id = %d',
           $table->getTableName(),
           $credential->getPHID(),
           $repository->getID());
 
         $edge_type = PhabricatorObjectUsesCredentialsEdgeType::EDGECONST;
 
         id(new PhabricatorEdgeEditor())
           ->addEdge($repository->getPHID(), $edge_type, $credential->getPHID())
           ->save();
       }
     }
   }
 }
 
 $table->saveTransaction();
 $passphrase->saveTransaction();
 
 echo pht('Done.')."\n";
diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php
index 14b61c314..e5a57a323 100644
--- a/src/__phutil_library_map__.php
+++ b/src/__phutil_library_map__.php
@@ -1,6873 +1,7147 @@
 <?php
 
 /**
  * This file is automatically generated. Use 'arc liberate' to rebuild it.
  *
  * @generated
  * @phutil-library-version 2
  */
 phutil_register_library_map(array(
   '__library_version__' => 2,
   'class' => array(
     'AlmanacAddress' => 'applications/almanac/util/AlmanacAddress.php',
     'AlmanacBinding' => 'applications/almanac/storage/AlmanacBinding.php',
     'AlmanacBindingEditController' => 'applications/almanac/controller/AlmanacBindingEditController.php',
     'AlmanacBindingEditor' => 'applications/almanac/editor/AlmanacBindingEditor.php',
     'AlmanacBindingPHIDType' => 'applications/almanac/phid/AlmanacBindingPHIDType.php',
     'AlmanacBindingQuery' => 'applications/almanac/query/AlmanacBindingQuery.php',
     'AlmanacBindingTableView' => 'applications/almanac/view/AlmanacBindingTableView.php',
     'AlmanacBindingTransaction' => 'applications/almanac/storage/AlmanacBindingTransaction.php',
     'AlmanacBindingTransactionQuery' => 'applications/almanac/query/AlmanacBindingTransactionQuery.php',
     'AlmanacBindingViewController' => 'applications/almanac/controller/AlmanacBindingViewController.php',
     'AlmanacClusterDatabaseServiceType' => 'applications/almanac/servicetype/AlmanacClusterDatabaseServiceType.php',
     'AlmanacClusterRepositoryServiceType' => 'applications/almanac/servicetype/AlmanacClusterRepositoryServiceType.php',
     'AlmanacClusterServiceType' => 'applications/almanac/servicetype/AlmanacClusterServiceType.php',
     'AlmanacConduitAPIMethod' => 'applications/almanac/conduit/AlmanacConduitAPIMethod.php',
     'AlmanacConsoleController' => 'applications/almanac/controller/AlmanacConsoleController.php',
     'AlmanacController' => 'applications/almanac/controller/AlmanacController.php',
     'AlmanacCoreCustomField' => 'applications/almanac/customfield/AlmanacCoreCustomField.php',
     'AlmanacCreateClusterServicesCapability' => 'applications/almanac/capability/AlmanacCreateClusterServicesCapability.php',
     'AlmanacCreateDevicesCapability' => 'applications/almanac/capability/AlmanacCreateDevicesCapability.php',
     'AlmanacCreateNetworksCapability' => 'applications/almanac/capability/AlmanacCreateNetworksCapability.php',
     'AlmanacCreateServicesCapability' => 'applications/almanac/capability/AlmanacCreateServicesCapability.php',
     'AlmanacCustomField' => 'applications/almanac/customfield/AlmanacCustomField.php',
     'AlmanacCustomServiceType' => 'applications/almanac/servicetype/AlmanacCustomServiceType.php',
     'AlmanacDAO' => 'applications/almanac/storage/AlmanacDAO.php',
     'AlmanacDevice' => 'applications/almanac/storage/AlmanacDevice.php',
     'AlmanacDeviceController' => 'applications/almanac/controller/AlmanacDeviceController.php',
     'AlmanacDeviceEditController' => 'applications/almanac/controller/AlmanacDeviceEditController.php',
     'AlmanacDeviceEditor' => 'applications/almanac/editor/AlmanacDeviceEditor.php',
     'AlmanacDeviceListController' => 'applications/almanac/controller/AlmanacDeviceListController.php',
     'AlmanacDevicePHIDType' => 'applications/almanac/phid/AlmanacDevicePHIDType.php',
     'AlmanacDeviceQuery' => 'applications/almanac/query/AlmanacDeviceQuery.php',
     'AlmanacDeviceSearchEngine' => 'applications/almanac/query/AlmanacDeviceSearchEngine.php',
     'AlmanacDeviceTransaction' => 'applications/almanac/storage/AlmanacDeviceTransaction.php',
     'AlmanacDeviceTransactionQuery' => 'applications/almanac/query/AlmanacDeviceTransactionQuery.php',
     'AlmanacDeviceViewController' => 'applications/almanac/controller/AlmanacDeviceViewController.php',
     'AlmanacInterface' => 'applications/almanac/storage/AlmanacInterface.php',
     'AlmanacInterfaceDatasource' => 'applications/almanac/typeahead/AlmanacInterfaceDatasource.php',
     'AlmanacInterfaceEditController' => 'applications/almanac/controller/AlmanacInterfaceEditController.php',
     'AlmanacInterfacePHIDType' => 'applications/almanac/phid/AlmanacInterfacePHIDType.php',
     'AlmanacInterfaceQuery' => 'applications/almanac/query/AlmanacInterfaceQuery.php',
     'AlmanacInterfaceTableView' => 'applications/almanac/view/AlmanacInterfaceTableView.php',
     'AlmanacKeys' => 'applications/almanac/util/AlmanacKeys.php',
     'AlmanacManagementLockWorkflow' => 'applications/almanac/management/AlmanacManagementLockWorkflow.php',
     'AlmanacManagementRegisterWorkflow' => 'applications/almanac/management/AlmanacManagementRegisterWorkflow.php',
     'AlmanacManagementTrustKeyWorkflow' => 'applications/almanac/management/AlmanacManagementTrustKeyWorkflow.php',
     'AlmanacManagementUnlockWorkflow' => 'applications/almanac/management/AlmanacManagementUnlockWorkflow.php',
     'AlmanacManagementUntrustKeyWorkflow' => 'applications/almanac/management/AlmanacManagementUntrustKeyWorkflow.php',
     'AlmanacManagementWorkflow' => 'applications/almanac/management/AlmanacManagementWorkflow.php',
     'AlmanacNames' => 'applications/almanac/util/AlmanacNames.php',
     'AlmanacNamesTestCase' => 'applications/almanac/util/__tests__/AlmanacNamesTestCase.php',
     'AlmanacNetwork' => 'applications/almanac/storage/AlmanacNetwork.php',
     'AlmanacNetworkController' => 'applications/almanac/controller/AlmanacNetworkController.php',
     'AlmanacNetworkEditController' => 'applications/almanac/controller/AlmanacNetworkEditController.php',
     'AlmanacNetworkEditor' => 'applications/almanac/editor/AlmanacNetworkEditor.php',
     'AlmanacNetworkListController' => 'applications/almanac/controller/AlmanacNetworkListController.php',
     'AlmanacNetworkPHIDType' => 'applications/almanac/phid/AlmanacNetworkPHIDType.php',
     'AlmanacNetworkQuery' => 'applications/almanac/query/AlmanacNetworkQuery.php',
     'AlmanacNetworkSearchEngine' => 'applications/almanac/query/AlmanacNetworkSearchEngine.php',
     'AlmanacNetworkTransaction' => 'applications/almanac/storage/AlmanacNetworkTransaction.php',
     'AlmanacNetworkTransactionQuery' => 'applications/almanac/query/AlmanacNetworkTransactionQuery.php',
     'AlmanacNetworkViewController' => 'applications/almanac/controller/AlmanacNetworkViewController.php',
     'AlmanacProperty' => 'applications/almanac/storage/AlmanacProperty.php',
     'AlmanacPropertyController' => 'applications/almanac/controller/AlmanacPropertyController.php',
     'AlmanacPropertyDeleteController' => 'applications/almanac/controller/AlmanacPropertyDeleteController.php',
     'AlmanacPropertyEditController' => 'applications/almanac/controller/AlmanacPropertyEditController.php',
     'AlmanacPropertyInterface' => 'applications/almanac/property/AlmanacPropertyInterface.php',
     'AlmanacPropertyQuery' => 'applications/almanac/query/AlmanacPropertyQuery.php',
     'AlmanacQuery' => 'applications/almanac/query/AlmanacQuery.php',
     'AlmanacQueryDevicesConduitAPIMethod' => 'applications/almanac/conduit/AlmanacQueryDevicesConduitAPIMethod.php',
     'AlmanacQueryServicesConduitAPIMethod' => 'applications/almanac/conduit/AlmanacQueryServicesConduitAPIMethod.php',
     'AlmanacSchemaSpec' => 'applications/almanac/storage/AlmanacSchemaSpec.php',
     'AlmanacService' => 'applications/almanac/storage/AlmanacService.php',
     'AlmanacServiceController' => 'applications/almanac/controller/AlmanacServiceController.php',
     'AlmanacServiceDatasource' => 'applications/almanac/typeahead/AlmanacServiceDatasource.php',
     'AlmanacServiceEditController' => 'applications/almanac/controller/AlmanacServiceEditController.php',
     'AlmanacServiceEditor' => 'applications/almanac/editor/AlmanacServiceEditor.php',
     'AlmanacServiceListController' => 'applications/almanac/controller/AlmanacServiceListController.php',
     'AlmanacServicePHIDType' => 'applications/almanac/phid/AlmanacServicePHIDType.php',
     'AlmanacServiceQuery' => 'applications/almanac/query/AlmanacServiceQuery.php',
     'AlmanacServiceSearchEngine' => 'applications/almanac/query/AlmanacServiceSearchEngine.php',
     'AlmanacServiceTransaction' => 'applications/almanac/storage/AlmanacServiceTransaction.php',
     'AlmanacServiceTransactionQuery' => 'applications/almanac/query/AlmanacServiceTransactionQuery.php',
     'AlmanacServiceType' => 'applications/almanac/servicetype/AlmanacServiceType.php',
+    'AlmanacServiceTypeTestCase' => 'applications/almanac/servicetype/__tests__/AlmanacServiceTypeTestCase.php',
     'AlmanacServiceViewController' => 'applications/almanac/controller/AlmanacServiceViewController.php',
     'AphlictDropdownDataQuery' => 'applications/aphlict/query/AphlictDropdownDataQuery.php',
     'Aphront304Response' => 'aphront/response/Aphront304Response.php',
     'Aphront400Response' => 'aphront/response/Aphront400Response.php',
     'Aphront403Response' => 'aphront/response/Aphront403Response.php',
     'Aphront404Response' => 'aphront/response/Aphront404Response.php',
     'AphrontAjaxResponse' => 'aphront/response/AphrontAjaxResponse.php',
     'AphrontApplicationConfiguration' => 'aphront/configuration/AphrontApplicationConfiguration.php',
     'AphrontBarView' => 'view/widget/bars/AphrontBarView.php',
     'AphrontCSRFException' => 'aphront/exception/AphrontCSRFException.php',
     'AphrontCalendarEventView' => 'applications/calendar/view/AphrontCalendarEventView.php',
     'AphrontController' => 'aphront/AphrontController.php',
     'AphrontCursorPagerView' => 'view/control/AphrontCursorPagerView.php',
     'AphrontDefaultApplicationConfiguration' => 'aphront/configuration/AphrontDefaultApplicationConfiguration.php',
     'AphrontDialogResponse' => 'aphront/response/AphrontDialogResponse.php',
     'AphrontDialogView' => 'view/AphrontDialogView.php',
     'AphrontException' => 'aphront/exception/AphrontException.php',
     'AphrontFileResponse' => 'aphront/response/AphrontFileResponse.php',
     'AphrontFormCheckboxControl' => 'view/form/control/AphrontFormCheckboxControl.php',
     'AphrontFormChooseButtonControl' => 'view/form/control/AphrontFormChooseButtonControl.php',
     'AphrontFormControl' => 'view/form/control/AphrontFormControl.php',
     'AphrontFormDateControl' => 'view/form/control/AphrontFormDateControl.php',
     'AphrontFormDateControlValue' => 'view/form/control/AphrontFormDateControlValue.php',
     'AphrontFormDividerControl' => 'view/form/control/AphrontFormDividerControl.php',
     'AphrontFormFileControl' => 'view/form/control/AphrontFormFileControl.php',
     'AphrontFormMarkupControl' => 'view/form/control/AphrontFormMarkupControl.php',
     'AphrontFormPasswordControl' => 'view/form/control/AphrontFormPasswordControl.php',
     'AphrontFormPolicyControl' => 'view/form/control/AphrontFormPolicyControl.php',
     'AphrontFormRadioButtonControl' => 'view/form/control/AphrontFormRadioButtonControl.php',
     'AphrontFormRecaptchaControl' => 'view/form/control/AphrontFormRecaptchaControl.php',
     'AphrontFormSelectControl' => 'view/form/control/AphrontFormSelectControl.php',
     'AphrontFormStaticControl' => 'view/form/control/AphrontFormStaticControl.php',
     'AphrontFormSubmitControl' => 'view/form/control/AphrontFormSubmitControl.php',
     'AphrontFormTextAreaControl' => 'view/form/control/AphrontFormTextAreaControl.php',
     'AphrontFormTextControl' => 'view/form/control/AphrontFormTextControl.php',
     'AphrontFormTextWithSubmitControl' => 'view/form/control/AphrontFormTextWithSubmitControl.php',
     'AphrontFormTokenizerControl' => 'view/form/control/AphrontFormTokenizerControl.php',
     'AphrontFormTypeaheadControl' => 'view/form/control/AphrontFormTypeaheadControl.php',
     'AphrontFormView' => 'view/form/AphrontFormView.php',
     'AphrontGlyphBarView' => 'view/widget/bars/AphrontGlyphBarView.php',
     'AphrontHTMLResponse' => 'aphront/response/AphrontHTMLResponse.php',
     'AphrontHTTPProxyResponse' => 'aphront/response/AphrontHTTPProxyResponse.php',
     'AphrontHTTPSink' => 'aphront/sink/AphrontHTTPSink.php',
     'AphrontHTTPSinkTestCase' => 'aphront/sink/__tests__/AphrontHTTPSinkTestCase.php',
     'AphrontIsolatedDatabaseConnectionTestCase' => 'infrastructure/storage/__tests__/AphrontIsolatedDatabaseConnectionTestCase.php',
     'AphrontIsolatedHTTPSink' => 'aphront/sink/AphrontIsolatedHTTPSink.php',
     'AphrontJSONResponse' => 'aphront/response/AphrontJSONResponse.php',
     'AphrontJavelinView' => 'view/AphrontJavelinView.php',
     'AphrontKeyboardShortcutsAvailableView' => 'view/widget/AphrontKeyboardShortcutsAvailableView.php',
     'AphrontListFilterView' => 'view/layout/AphrontListFilterView.php',
     'AphrontMoreView' => 'view/layout/AphrontMoreView.php',
     'AphrontMultiColumnView' => 'view/layout/AphrontMultiColumnView.php',
     'AphrontMySQLDatabaseConnectionTestCase' => 'infrastructure/storage/__tests__/AphrontMySQLDatabaseConnectionTestCase.php',
     'AphrontNullView' => 'view/AphrontNullView.php',
     'AphrontPHPHTTPSink' => 'aphront/sink/AphrontPHPHTTPSink.php',
     'AphrontPageView' => 'view/page/AphrontPageView.php',
     'AphrontPlainTextResponse' => 'aphront/response/AphrontPlainTextResponse.php',
     'AphrontProgressBarView' => 'view/widget/bars/AphrontProgressBarView.php',
     'AphrontProxyResponse' => 'aphront/response/AphrontProxyResponse.php',
     'AphrontRedirectResponse' => 'aphront/response/AphrontRedirectResponse.php',
     'AphrontRedirectResponseTestCase' => 'aphront/response/__tests__/AphrontRedirectResponseTestCase.php',
     'AphrontReloadResponse' => 'aphront/response/AphrontReloadResponse.php',
     'AphrontRequest' => 'aphront/AphrontRequest.php',
     'AphrontRequestTestCase' => 'aphront/__tests__/AphrontRequestTestCase.php',
     'AphrontResponse' => 'aphront/response/AphrontResponse.php',
     'AphrontSideNavFilterView' => 'view/layout/AphrontSideNavFilterView.php',
     'AphrontStackTraceView' => 'view/widget/AphrontStackTraceView.php',
     'AphrontStandaloneHTMLResponse' => 'aphront/response/AphrontStandaloneHTMLResponse.php',
     'AphrontTableView' => 'view/control/AphrontTableView.php',
     'AphrontTagView' => 'view/AphrontTagView.php',
     'AphrontTokenizerTemplateView' => 'view/control/AphrontTokenizerTemplateView.php',
     'AphrontTwoColumnView' => 'view/layout/AphrontTwoColumnView.php',
     'AphrontTypeaheadTemplateView' => 'view/control/AphrontTypeaheadTemplateView.php',
     'AphrontURIMapper' => 'aphront/AphrontURIMapper.php',
     'AphrontUnhandledExceptionResponse' => 'aphront/response/AphrontUnhandledExceptionResponse.php',
     'AphrontUsageException' => 'aphront/exception/AphrontUsageException.php',
     'AphrontView' => 'view/AphrontView.php',
     'AphrontWebpageResponse' => 'aphront/response/AphrontWebpageResponse.php',
     'ArcanistConduitAPIMethod' => 'applications/arcanist/conduit/ArcanistConduitAPIMethod.php',
     'ArcanistProjectInfoConduitAPIMethod' => 'applications/arcanist/conduit/ArcanistProjectInfoConduitAPIMethod.php',
     'AuditConduitAPIMethod' => 'applications/audit/conduit/AuditConduitAPIMethod.php',
     'AuditQueryConduitAPIMethod' => 'applications/audit/conduit/AuditQueryConduitAPIMethod.php',
     'AuthManageProvidersCapability' => 'applications/auth/capability/AuthManageProvidersCapability.php',
     'CalendarTimeUtil' => 'applications/calendar/util/CalendarTimeUtil.php',
     'CalendarTimeUtilTestCase' => 'applications/calendar/__tests__/CalendarTimeUtilTestCase.php',
     'CelerityAPI' => 'applications/celerity/CelerityAPI.php',
     'CelerityManagementMapWorkflow' => 'applications/celerity/management/CelerityManagementMapWorkflow.php',
     'CelerityManagementWorkflow' => 'applications/celerity/management/CelerityManagementWorkflow.php',
     'CelerityPhabricatorResourceController' => 'applications/celerity/controller/CelerityPhabricatorResourceController.php',
     'CelerityPhabricatorResources' => 'applications/celerity/resources/CelerityPhabricatorResources.php',
     'CelerityPhysicalResources' => 'applications/celerity/resources/CelerityPhysicalResources.php',
+    'CelerityPhysicalResourcesTestCase' => 'applications/celerity/resources/__tests__/CelerityPhysicalResourcesTestCase.php',
     'CelerityResourceController' => 'applications/celerity/controller/CelerityResourceController.php',
     'CelerityResourceGraph' => 'applications/celerity/CelerityResourceGraph.php',
     'CelerityResourceMap' => 'applications/celerity/CelerityResourceMap.php',
     'CelerityResourceMapGenerator' => 'applications/celerity/CelerityResourceMapGenerator.php',
     'CelerityResourceTransformer' => 'applications/celerity/CelerityResourceTransformer.php',
     'CelerityResourceTransformerTestCase' => 'applications/celerity/__tests__/CelerityResourceTransformerTestCase.php',
     'CelerityResources' => 'applications/celerity/resources/CelerityResources.php',
     'CelerityResourcesOnDisk' => 'applications/celerity/resources/CelerityResourcesOnDisk.php',
     'CeleritySpriteGenerator' => 'applications/celerity/CeleritySpriteGenerator.php',
     'CelerityStaticResourceResponse' => 'applications/celerity/CelerityStaticResourceResponse.php',
     'ChatLogConduitAPIMethod' => 'applications/chatlog/conduit/ChatLogConduitAPIMethod.php',
     'ChatLogQueryConduitAPIMethod' => 'applications/chatlog/conduit/ChatLogQueryConduitAPIMethod.php',
     'ChatLogRecordConduitAPIMethod' => 'applications/chatlog/conduit/ChatLogRecordConduitAPIMethod.php',
     'ConduitAPIMethod' => 'applications/conduit/method/ConduitAPIMethod.php',
+    'ConduitAPIMethodTestCase' => 'applications/conduit/method/__tests__/ConduitAPIMethodTestCase.php',
     'ConduitAPIRequest' => 'applications/conduit/protocol/ConduitAPIRequest.php',
     'ConduitAPIResponse' => 'applications/conduit/protocol/ConduitAPIResponse.php',
     'ConduitApplicationNotInstalledException' => 'applications/conduit/protocol/exception/ConduitApplicationNotInstalledException.php',
     'ConduitCall' => 'applications/conduit/call/ConduitCall.php',
     'ConduitCallTestCase' => 'applications/conduit/call/__tests__/ConduitCallTestCase.php',
     'ConduitConnectConduitAPIMethod' => 'applications/conduit/method/ConduitConnectConduitAPIMethod.php',
     'ConduitConnectionGarbageCollector' => 'applications/conduit/garbagecollector/ConduitConnectionGarbageCollector.php',
     'ConduitDeprecatedCallSetupCheck' => 'applications/conduit/check/ConduitDeprecatedCallSetupCheck.php',
     'ConduitException' => 'applications/conduit/protocol/exception/ConduitException.php',
     'ConduitGetCapabilitiesConduitAPIMethod' => 'applications/conduit/method/ConduitGetCapabilitiesConduitAPIMethod.php',
     'ConduitGetCertificateConduitAPIMethod' => 'applications/conduit/method/ConduitGetCertificateConduitAPIMethod.php',
     'ConduitLogGarbageCollector' => 'applications/conduit/garbagecollector/ConduitLogGarbageCollector.php',
     'ConduitMethodDoesNotExistException' => 'applications/conduit/protocol/exception/ConduitMethodDoesNotExistException.php',
     'ConduitMethodNotFoundException' => 'applications/conduit/protocol/exception/ConduitMethodNotFoundException.php',
     'ConduitPingConduitAPIMethod' => 'applications/conduit/method/ConduitPingConduitAPIMethod.php',
     'ConduitQueryConduitAPIMethod' => 'applications/conduit/method/ConduitQueryConduitAPIMethod.php',
     'ConduitSSHWorkflow' => 'applications/conduit/ssh/ConduitSSHWorkflow.php',
     'ConduitTokenGarbageCollector' => 'applications/conduit/garbagecollector/ConduitTokenGarbageCollector.php',
     'ConpherenceColumnViewController' => 'applications/conpherence/controller/ConpherenceColumnViewController.php',
     'ConpherenceConduitAPIMethod' => 'applications/conpherence/conduit/ConpherenceConduitAPIMethod.php',
     'ConpherenceConfigOptions' => 'applications/conpherence/config/ConpherenceConfigOptions.php',
     'ConpherenceConstants' => 'applications/conpherence/constants/ConpherenceConstants.php',
     'ConpherenceController' => 'applications/conpherence/controller/ConpherenceController.php',
     'ConpherenceCreateThreadConduitAPIMethod' => 'applications/conpherence/conduit/ConpherenceCreateThreadConduitAPIMethod.php',
     'ConpherenceCreateThreadMailReceiver' => 'applications/conpherence/mail/ConpherenceCreateThreadMailReceiver.php',
     'ConpherenceDAO' => 'applications/conpherence/storage/ConpherenceDAO.php',
     'ConpherenceDurableColumnView' => 'applications/conpherence/view/ConpherenceDurableColumnView.php',
     'ConpherenceEditor' => 'applications/conpherence/editor/ConpherenceEditor.php',
     'ConpherenceFileWidgetView' => 'applications/conpherence/view/ConpherenceFileWidgetView.php',
     'ConpherenceFormDragAndDropUploadControl' => 'applications/conpherence/view/ConpherenceFormDragAndDropUploadControl.php',
     'ConpherenceFulltextQuery' => 'applications/conpherence/query/ConpherenceFulltextQuery.php',
     'ConpherenceHovercardEventListener' => 'applications/conpherence/events/ConpherenceHovercardEventListener.php',
     'ConpherenceImageData' => 'applications/conpherence/constants/ConpherenceImageData.php',
     'ConpherenceIndex' => 'applications/conpherence/storage/ConpherenceIndex.php',
     'ConpherenceLayoutView' => 'applications/conpherence/view/ConpherenceLayoutView.php',
     'ConpherenceListController' => 'applications/conpherence/controller/ConpherenceListController.php',
     'ConpherenceMenuItemView' => 'applications/conpherence/view/ConpherenceMenuItemView.php',
     'ConpherenceNewController' => 'applications/conpherence/controller/ConpherenceNewController.php',
     'ConpherenceNewRoomController' => 'applications/conpherence/controller/ConpherenceNewRoomController.php',
     'ConpherenceNotificationPanelController' => 'applications/conpherence/controller/ConpherenceNotificationPanelController.php',
     'ConpherenceParticipant' => 'applications/conpherence/storage/ConpherenceParticipant.php',
     'ConpherenceParticipantCountQuery' => 'applications/conpherence/query/ConpherenceParticipantCountQuery.php',
     'ConpherenceParticipantQuery' => 'applications/conpherence/query/ConpherenceParticipantQuery.php',
     'ConpherenceParticipationStatus' => 'applications/conpherence/constants/ConpherenceParticipationStatus.php',
     'ConpherencePeopleWidgetView' => 'applications/conpherence/view/ConpherencePeopleWidgetView.php',
     'ConpherencePicCropControl' => 'applications/conpherence/view/ConpherencePicCropControl.php',
     'ConpherenceQueryThreadConduitAPIMethod' => 'applications/conpherence/conduit/ConpherenceQueryThreadConduitAPIMethod.php',
     'ConpherenceQueryTransactionConduitAPIMethod' => 'applications/conpherence/conduit/ConpherenceQueryTransactionConduitAPIMethod.php',
     'ConpherenceReplyHandler' => 'applications/conpherence/mail/ConpherenceReplyHandler.php',
     'ConpherenceRoomListController' => 'applications/conpherence/controller/ConpherenceRoomListController.php',
     'ConpherenceRoomTestCase' => 'applications/conpherence/__tests__/ConpherenceRoomTestCase.php',
     'ConpherenceSchemaSpec' => 'applications/conpherence/storage/ConpherenceSchemaSpec.php',
     'ConpherenceSettings' => 'applications/conpherence/constants/ConpherenceSettings.php',
     'ConpherenceTestCase' => 'applications/conpherence/__tests__/ConpherenceTestCase.php',
     'ConpherenceThread' => 'applications/conpherence/storage/ConpherenceThread.php',
     'ConpherenceThreadIndexer' => 'applications/conpherence/search/ConpherenceThreadIndexer.php',
     'ConpherenceThreadListView' => 'applications/conpherence/view/ConpherenceThreadListView.php',
     'ConpherenceThreadMailReceiver' => 'applications/conpherence/mail/ConpherenceThreadMailReceiver.php',
+    'ConpherenceThreadMembersPolicyRule' => 'applications/conpherence/policyrule/ConpherenceThreadMembersPolicyRule.php',
     'ConpherenceThreadQuery' => 'applications/conpherence/query/ConpherenceThreadQuery.php',
     'ConpherenceThreadRemarkupRule' => 'applications/conpherence/remarkup/ConpherenceThreadRemarkupRule.php',
     'ConpherenceThreadSearchEngine' => 'applications/conpherence/query/ConpherenceThreadSearchEngine.php',
     'ConpherenceThreadTestCase' => 'applications/conpherence/__tests__/ConpherenceThreadTestCase.php',
     'ConpherenceTransaction' => 'applications/conpherence/storage/ConpherenceTransaction.php',
     'ConpherenceTransactionComment' => 'applications/conpherence/storage/ConpherenceTransactionComment.php',
     'ConpherenceTransactionQuery' => 'applications/conpherence/query/ConpherenceTransactionQuery.php',
     'ConpherenceTransactionRenderer' => 'applications/conpherence/ConpherenceTransactionRenderer.php',
     'ConpherenceTransactionView' => 'applications/conpherence/view/ConpherenceTransactionView.php',
     'ConpherenceUpdateActions' => 'applications/conpherence/constants/ConpherenceUpdateActions.php',
     'ConpherenceUpdateController' => 'applications/conpherence/controller/ConpherenceUpdateController.php',
     'ConpherenceUpdateThreadConduitAPIMethod' => 'applications/conpherence/conduit/ConpherenceUpdateThreadConduitAPIMethod.php',
     'ConpherenceViewController' => 'applications/conpherence/controller/ConpherenceViewController.php',
     'ConpherenceWidgetConfigConstants' => 'applications/conpherence/constants/ConpherenceWidgetConfigConstants.php',
     'ConpherenceWidgetController' => 'applications/conpherence/controller/ConpherenceWidgetController.php',
     'ConpherenceWidgetView' => 'applications/conpherence/view/ConpherenceWidgetView.php',
     'DarkConsoleController' => 'applications/console/controller/DarkConsoleController.php',
     'DarkConsoleCore' => 'applications/console/core/DarkConsoleCore.php',
     'DarkConsoleDataController' => 'applications/console/controller/DarkConsoleDataController.php',
     'DarkConsoleErrorLogPlugin' => 'applications/console/plugin/DarkConsoleErrorLogPlugin.php',
     'DarkConsoleErrorLogPluginAPI' => 'applications/console/plugin/errorlog/DarkConsoleErrorLogPluginAPI.php',
     'DarkConsoleEventPlugin' => 'applications/console/plugin/DarkConsoleEventPlugin.php',
     'DarkConsoleEventPluginAPI' => 'applications/console/plugin/event/DarkConsoleEventPluginAPI.php',
     'DarkConsolePlugin' => 'applications/console/plugin/DarkConsolePlugin.php',
     'DarkConsoleRequestPlugin' => 'applications/console/plugin/DarkConsoleRequestPlugin.php',
     'DarkConsoleServicesPlugin' => 'applications/console/plugin/DarkConsoleServicesPlugin.php',
     'DarkConsoleXHProfPlugin' => 'applications/console/plugin/DarkConsoleXHProfPlugin.php',
     'DarkConsoleXHProfPluginAPI' => 'applications/console/plugin/xhprof/DarkConsoleXHProfPluginAPI.php',
     'DatabaseConfigurationProvider' => 'infrastructure/storage/configuration/DatabaseConfigurationProvider.php',
     'DefaultDatabaseConfigurationProvider' => 'infrastructure/storage/configuration/DefaultDatabaseConfigurationProvider.php',
     'DifferentialAction' => 'applications/differential/constants/DifferentialAction.php',
     'DifferentialActionEmailCommand' => 'applications/differential/command/DifferentialActionEmailCommand.php',
     'DifferentialActionMenuEventListener' => 'applications/differential/event/DifferentialActionMenuEventListener.php',
     'DifferentialAddCommentView' => 'applications/differential/view/DifferentialAddCommentView.php',
     'DifferentialAdjustmentMapTestCase' => 'applications/differential/storage/__tests__/DifferentialAdjustmentMapTestCase.php',
     'DifferentialAffectedPath' => 'applications/differential/storage/DifferentialAffectedPath.php',
     'DifferentialApplyPatchField' => 'applications/differential/customfield/DifferentialApplyPatchField.php',
     'DifferentialAsanaRepresentationField' => 'applications/differential/customfield/DifferentialAsanaRepresentationField.php',
     'DifferentialAuditorsField' => 'applications/differential/customfield/DifferentialAuditorsField.php',
     'DifferentialAuthorField' => 'applications/differential/customfield/DifferentialAuthorField.php',
     'DifferentialBlameRevisionField' => 'applications/differential/customfield/DifferentialBlameRevisionField.php',
     'DifferentialBranchField' => 'applications/differential/customfield/DifferentialBranchField.php',
     'DifferentialChangeType' => 'applications/differential/constants/DifferentialChangeType.php',
     'DifferentialChangesSinceLastUpdateField' => 'applications/differential/customfield/DifferentialChangesSinceLastUpdateField.php',
     'DifferentialChangeset' => 'applications/differential/storage/DifferentialChangeset.php',
     'DifferentialChangesetDetailView' => 'applications/differential/view/DifferentialChangesetDetailView.php',
     'DifferentialChangesetFileTreeSideNavBuilder' => 'applications/differential/view/DifferentialChangesetFileTreeSideNavBuilder.php',
     'DifferentialChangesetHTMLRenderer' => 'applications/differential/render/DifferentialChangesetHTMLRenderer.php',
     'DifferentialChangesetListView' => 'applications/differential/view/DifferentialChangesetListView.php',
     'DifferentialChangesetOneUpRenderer' => 'applications/differential/render/DifferentialChangesetOneUpRenderer.php',
     'DifferentialChangesetOneUpTestRenderer' => 'applications/differential/render/DifferentialChangesetOneUpTestRenderer.php',
     'DifferentialChangesetParser' => 'applications/differential/parser/DifferentialChangesetParser.php',
     'DifferentialChangesetParserTestCase' => 'applications/differential/parser/__tests__/DifferentialChangesetParserTestCase.php',
     'DifferentialChangesetQuery' => 'applications/differential/query/DifferentialChangesetQuery.php',
     'DifferentialChangesetRenderer' => 'applications/differential/render/DifferentialChangesetRenderer.php',
     'DifferentialChangesetTestRenderer' => 'applications/differential/render/DifferentialChangesetTestRenderer.php',
     'DifferentialChangesetTwoUpRenderer' => 'applications/differential/render/DifferentialChangesetTwoUpRenderer.php',
     'DifferentialChangesetTwoUpTestRenderer' => 'applications/differential/render/DifferentialChangesetTwoUpTestRenderer.php',
     'DifferentialChangesetViewController' => 'applications/differential/controller/DifferentialChangesetViewController.php',
     'DifferentialCloseConduitAPIMethod' => 'applications/differential/conduit/DifferentialCloseConduitAPIMethod.php',
     'DifferentialCommentPreviewController' => 'applications/differential/controller/DifferentialCommentPreviewController.php',
     'DifferentialCommentSaveController' => 'applications/differential/controller/DifferentialCommentSaveController.php',
     'DifferentialCommitMessageParser' => 'applications/differential/parser/DifferentialCommitMessageParser.php',
     'DifferentialCommitMessageParserTestCase' => 'applications/differential/parser/__tests__/DifferentialCommitMessageParserTestCase.php',
     'DifferentialCommitsField' => 'applications/differential/customfield/DifferentialCommitsField.php',
     'DifferentialConduitAPIMethod' => 'applications/differential/conduit/DifferentialConduitAPIMethod.php',
     'DifferentialConflictsField' => 'applications/differential/customfield/DifferentialConflictsField.php',
     'DifferentialController' => 'applications/differential/controller/DifferentialController.php',
     'DifferentialCoreCustomField' => 'applications/differential/customfield/DifferentialCoreCustomField.php',
     'DifferentialCreateCommentConduitAPIMethod' => 'applications/differential/conduit/DifferentialCreateCommentConduitAPIMethod.php',
     'DifferentialCreateDiffConduitAPIMethod' => 'applications/differential/conduit/DifferentialCreateDiffConduitAPIMethod.php',
     'DifferentialCreateInlineConduitAPIMethod' => 'applications/differential/conduit/DifferentialCreateInlineConduitAPIMethod.php',
     'DifferentialCreateMailReceiver' => 'applications/differential/mail/DifferentialCreateMailReceiver.php',
     'DifferentialCreateRawDiffConduitAPIMethod' => 'applications/differential/conduit/DifferentialCreateRawDiffConduitAPIMethod.php',
     'DifferentialCreateRevisionConduitAPIMethod' => 'applications/differential/conduit/DifferentialCreateRevisionConduitAPIMethod.php',
     'DifferentialCustomField' => 'applications/differential/customfield/DifferentialCustomField.php',
     'DifferentialCustomFieldDependsOnParser' => 'applications/differential/parser/DifferentialCustomFieldDependsOnParser.php',
     'DifferentialCustomFieldDependsOnParserTestCase' => 'applications/differential/parser/__tests__/DifferentialCustomFieldDependsOnParserTestCase.php',
     'DifferentialCustomFieldNumericIndex' => 'applications/differential/storage/DifferentialCustomFieldNumericIndex.php',
     'DifferentialCustomFieldRevertsParser' => 'applications/differential/parser/DifferentialCustomFieldRevertsParser.php',
     'DifferentialCustomFieldRevertsParserTestCase' => 'applications/differential/parser/__tests__/DifferentialCustomFieldRevertsParserTestCase.php',
     'DifferentialCustomFieldStorage' => 'applications/differential/storage/DifferentialCustomFieldStorage.php',
     'DifferentialCustomFieldStringIndex' => 'applications/differential/storage/DifferentialCustomFieldStringIndex.php',
     'DifferentialDAO' => 'applications/differential/storage/DifferentialDAO.php',
     'DifferentialDefaultViewCapability' => 'applications/differential/capability/DifferentialDefaultViewCapability.php',
     'DifferentialDependenciesField' => 'applications/differential/customfield/DifferentialDependenciesField.php',
     'DifferentialDependsOnField' => 'applications/differential/customfield/DifferentialDependsOnField.php',
     'DifferentialDiff' => 'applications/differential/storage/DifferentialDiff.php',
     'DifferentialDiffCreateController' => 'applications/differential/controller/DifferentialDiffCreateController.php',
     'DifferentialDiffEditor' => 'applications/differential/editor/DifferentialDiffEditor.php',
     'DifferentialDiffInlineCommentQuery' => 'applications/differential/query/DifferentialDiffInlineCommentQuery.php',
     'DifferentialDiffPHIDType' => 'applications/differential/phid/DifferentialDiffPHIDType.php',
     'DifferentialDiffProperty' => 'applications/differential/storage/DifferentialDiffProperty.php',
     'DifferentialDiffQuery' => 'applications/differential/query/DifferentialDiffQuery.php',
     'DifferentialDiffTableOfContentsView' => 'applications/differential/view/DifferentialDiffTableOfContentsView.php',
     'DifferentialDiffTestCase' => 'applications/differential/storage/__tests__/DifferentialDiffTestCase.php',
     'DifferentialDiffTransaction' => 'applications/differential/storage/DifferentialDiffTransaction.php',
     'DifferentialDiffViewController' => 'applications/differential/controller/DifferentialDiffViewController.php',
     'DifferentialDoorkeeperRevisionFeedStoryPublisher' => 'applications/differential/doorkeeper/DifferentialDoorkeeperRevisionFeedStoryPublisher.php',
     'DifferentialDraft' => 'applications/differential/storage/DifferentialDraft.php',
     'DifferentialEditPolicyField' => 'applications/differential/customfield/DifferentialEditPolicyField.php',
     'DifferentialFieldParseException' => 'applications/differential/exception/DifferentialFieldParseException.php',
     'DifferentialFieldValidationException' => 'applications/differential/exception/DifferentialFieldValidationException.php',
     'DifferentialFindConduitAPIMethod' => 'applications/differential/conduit/DifferentialFindConduitAPIMethod.php',
     'DifferentialFinishPostponedLintersConduitAPIMethod' => 'applications/differential/conduit/DifferentialFinishPostponedLintersConduitAPIMethod.php',
     'DifferentialGetAllDiffsConduitAPIMethod' => 'applications/differential/conduit/DifferentialGetAllDiffsConduitAPIMethod.php',
     'DifferentialGetCommitMessageConduitAPIMethod' => 'applications/differential/conduit/DifferentialGetCommitMessageConduitAPIMethod.php',
     'DifferentialGetCommitPathsConduitAPIMethod' => 'applications/differential/conduit/DifferentialGetCommitPathsConduitAPIMethod.php',
     'DifferentialGetDiffConduitAPIMethod' => 'applications/differential/conduit/DifferentialGetDiffConduitAPIMethod.php',
     'DifferentialGetRawDiffConduitAPIMethod' => 'applications/differential/conduit/DifferentialGetRawDiffConduitAPIMethod.php',
     'DifferentialGetRevisionCommentsConduitAPIMethod' => 'applications/differential/conduit/DifferentialGetRevisionCommentsConduitAPIMethod.php',
     'DifferentialGetRevisionConduitAPIMethod' => 'applications/differential/conduit/DifferentialGetRevisionConduitAPIMethod.php',
     'DifferentialGetWorkingCopy' => 'applications/differential/DifferentialGetWorkingCopy.php',
     'DifferentialGitHubLandingStrategy' => 'applications/differential/landing/DifferentialGitHubLandingStrategy.php',
     'DifferentialGitSVNIDField' => 'applications/differential/customfield/DifferentialGitSVNIDField.php',
     'DifferentialHiddenComment' => 'applications/differential/storage/DifferentialHiddenComment.php',
     'DifferentialHostField' => 'applications/differential/customfield/DifferentialHostField.php',
     'DifferentialHostedGitLandingStrategy' => 'applications/differential/landing/DifferentialHostedGitLandingStrategy.php',
     'DifferentialHostedMercurialLandingStrategy' => 'applications/differential/landing/DifferentialHostedMercurialLandingStrategy.php',
     'DifferentialHovercardEventListener' => 'applications/differential/event/DifferentialHovercardEventListener.php',
     'DifferentialHunk' => 'applications/differential/storage/DifferentialHunk.php',
     'DifferentialHunkParser' => 'applications/differential/parser/DifferentialHunkParser.php',
     'DifferentialHunkParserTestCase' => 'applications/differential/parser/__tests__/DifferentialHunkParserTestCase.php',
     'DifferentialHunkQuery' => 'applications/differential/query/DifferentialHunkQuery.php',
     'DifferentialHunkTestCase' => 'applications/differential/storage/__tests__/DifferentialHunkTestCase.php',
     'DifferentialInlineComment' => 'applications/differential/storage/DifferentialInlineComment.php',
     'DifferentialInlineCommentEditController' => 'applications/differential/controller/DifferentialInlineCommentEditController.php',
     'DifferentialInlineCommentPreviewController' => 'applications/differential/controller/DifferentialInlineCommentPreviewController.php',
     'DifferentialInlineCommentQuery' => 'applications/differential/query/DifferentialInlineCommentQuery.php',
     'DifferentialJIRAIssuesField' => 'applications/differential/customfield/DifferentialJIRAIssuesField.php',
     'DifferentialLandingActionMenuEventListener' => 'applications/differential/landing/DifferentialLandingActionMenuEventListener.php',
     'DifferentialLandingStrategy' => 'applications/differential/landing/DifferentialLandingStrategy.php',
     'DifferentialLegacyHunk' => 'applications/differential/storage/DifferentialLegacyHunk.php',
     'DifferentialLineAdjustmentMap' => 'applications/differential/parser/DifferentialLineAdjustmentMap.php',
     'DifferentialLintField' => 'applications/differential/customfield/DifferentialLintField.php',
     'DifferentialLintStatus' => 'applications/differential/constants/DifferentialLintStatus.php',
     'DifferentialLocalCommitsView' => 'applications/differential/view/DifferentialLocalCommitsView.php',
     'DifferentialManiphestTasksField' => 'applications/differential/customfield/DifferentialManiphestTasksField.php',
     'DifferentialModernHunk' => 'applications/differential/storage/DifferentialModernHunk.php',
     'DifferentialParseCacheGarbageCollector' => 'applications/differential/garbagecollector/DifferentialParseCacheGarbageCollector.php',
     'DifferentialParseCommitMessageConduitAPIMethod' => 'applications/differential/conduit/DifferentialParseCommitMessageConduitAPIMethod.php',
     'DifferentialParseRenderTestCase' => 'applications/differential/__tests__/DifferentialParseRenderTestCase.php',
     'DifferentialPathField' => 'applications/differential/customfield/DifferentialPathField.php',
     'DifferentialPrimaryPaneView' => 'applications/differential/view/DifferentialPrimaryPaneView.php',
     'DifferentialProjectReviewersField' => 'applications/differential/customfield/DifferentialProjectReviewersField.php',
     'DifferentialProjectsField' => 'applications/differential/customfield/DifferentialProjectsField.php',
     'DifferentialQueryConduitAPIMethod' => 'applications/differential/conduit/DifferentialQueryConduitAPIMethod.php',
     'DifferentialQueryDiffsConduitAPIMethod' => 'applications/differential/conduit/DifferentialQueryDiffsConduitAPIMethod.php',
     'DifferentialRawDiffRenderer' => 'applications/differential/render/DifferentialRawDiffRenderer.php',
     'DifferentialReleephRequestFieldSpecification' => 'applications/releeph/differential/DifferentialReleephRequestFieldSpecification.php',
     'DifferentialRemarkupRule' => 'applications/differential/remarkup/DifferentialRemarkupRule.php',
     'DifferentialReplyHandler' => 'applications/differential/mail/DifferentialReplyHandler.php',
     'DifferentialRepositoryField' => 'applications/differential/customfield/DifferentialRepositoryField.php',
     'DifferentialRepositoryLookup' => 'applications/differential/query/DifferentialRepositoryLookup.php',
     'DifferentialRequiredSignaturesField' => 'applications/differential/customfield/DifferentialRequiredSignaturesField.php',
     'DifferentialResultsTableView' => 'applications/differential/view/DifferentialResultsTableView.php',
     'DifferentialRevertPlanField' => 'applications/differential/customfield/DifferentialRevertPlanField.php',
     'DifferentialReviewedByField' => 'applications/differential/customfield/DifferentialReviewedByField.php',
     'DifferentialReviewer' => 'applications/differential/storage/DifferentialReviewer.php',
     'DifferentialReviewerForRevisionEdgeType' => 'applications/differential/edge/DifferentialReviewerForRevisionEdgeType.php',
     'DifferentialReviewerStatus' => 'applications/differential/constants/DifferentialReviewerStatus.php',
     'DifferentialReviewersField' => 'applications/differential/customfield/DifferentialReviewersField.php',
     'DifferentialReviewersView' => 'applications/differential/view/DifferentialReviewersView.php',
     'DifferentialRevision' => 'applications/differential/storage/DifferentialRevision.php',
     'DifferentialRevisionCloseDetailsController' => 'applications/differential/controller/DifferentialRevisionCloseDetailsController.php',
     'DifferentialRevisionControlSystem' => 'applications/differential/constants/DifferentialRevisionControlSystem.php',
     'DifferentialRevisionDependedOnByRevisionEdgeType' => 'applications/differential/edge/DifferentialRevisionDependedOnByRevisionEdgeType.php',
     'DifferentialRevisionDependsOnRevisionEdgeType' => 'applications/differential/edge/DifferentialRevisionDependsOnRevisionEdgeType.php',
     'DifferentialRevisionDetailView' => 'applications/differential/view/DifferentialRevisionDetailView.php',
     'DifferentialRevisionEditController' => 'applications/differential/controller/DifferentialRevisionEditController.php',
     'DifferentialRevisionHasCommitEdgeType' => 'applications/differential/edge/DifferentialRevisionHasCommitEdgeType.php',
     'DifferentialRevisionHasReviewerEdgeType' => 'applications/differential/edge/DifferentialRevisionHasReviewerEdgeType.php',
     'DifferentialRevisionHasTaskEdgeType' => 'applications/differential/edge/DifferentialRevisionHasTaskEdgeType.php',
     'DifferentialRevisionIDField' => 'applications/differential/customfield/DifferentialRevisionIDField.php',
     'DifferentialRevisionLandController' => 'applications/differential/controller/DifferentialRevisionLandController.php',
     'DifferentialRevisionListController' => 'applications/differential/controller/DifferentialRevisionListController.php',
     'DifferentialRevisionListView' => 'applications/differential/view/DifferentialRevisionListView.php',
     'DifferentialRevisionMailReceiver' => 'applications/differential/mail/DifferentialRevisionMailReceiver.php',
     'DifferentialRevisionPHIDType' => 'applications/differential/phid/DifferentialRevisionPHIDType.php',
     'DifferentialRevisionQuery' => 'applications/differential/query/DifferentialRevisionQuery.php',
     'DifferentialRevisionSearchEngine' => 'applications/differential/query/DifferentialRevisionSearchEngine.php',
     'DifferentialRevisionStatus' => 'applications/differential/constants/DifferentialRevisionStatus.php',
     'DifferentialRevisionUpdateHistoryView' => 'applications/differential/view/DifferentialRevisionUpdateHistoryView.php',
     'DifferentialRevisionViewController' => 'applications/differential/controller/DifferentialRevisionViewController.php',
     'DifferentialSchemaSpec' => 'applications/differential/storage/DifferentialSchemaSpec.php',
     'DifferentialSearchIndexer' => 'applications/differential/search/DifferentialSearchIndexer.php',
     'DifferentialSetDiffPropertyConduitAPIMethod' => 'applications/differential/conduit/DifferentialSetDiffPropertyConduitAPIMethod.php',
     'DifferentialStoredCustomField' => 'applications/differential/customfield/DifferentialStoredCustomField.php',
     'DifferentialSubscribersField' => 'applications/differential/customfield/DifferentialSubscribersField.php',
     'DifferentialSummaryField' => 'applications/differential/customfield/DifferentialSummaryField.php',
     'DifferentialTestPlanField' => 'applications/differential/customfield/DifferentialTestPlanField.php',
     'DifferentialTitleField' => 'applications/differential/customfield/DifferentialTitleField.php',
     'DifferentialTransaction' => 'applications/differential/storage/DifferentialTransaction.php',
     'DifferentialTransactionComment' => 'applications/differential/storage/DifferentialTransactionComment.php',
     'DifferentialTransactionEditor' => 'applications/differential/editor/DifferentialTransactionEditor.php',
     'DifferentialTransactionQuery' => 'applications/differential/query/DifferentialTransactionQuery.php',
     'DifferentialTransactionView' => 'applications/differential/view/DifferentialTransactionView.php',
     'DifferentialUnitField' => 'applications/differential/customfield/DifferentialUnitField.php',
     'DifferentialUnitStatus' => 'applications/differential/constants/DifferentialUnitStatus.php',
     'DifferentialUnitTestResult' => 'applications/differential/constants/DifferentialUnitTestResult.php',
     'DifferentialUpdateRevisionConduitAPIMethod' => 'applications/differential/conduit/DifferentialUpdateRevisionConduitAPIMethod.php',
     'DifferentialUpdateUnitResultsConduitAPIMethod' => 'applications/differential/conduit/DifferentialUpdateUnitResultsConduitAPIMethod.php',
     'DifferentialViewPolicyField' => 'applications/differential/customfield/DifferentialViewPolicyField.php',
     'DiffusionAuditorDatasource' => 'applications/diffusion/typeahead/DiffusionAuditorDatasource.php',
     'DiffusionBranchQueryConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionBranchQueryConduitAPIMethod.php',
     'DiffusionBranchTableController' => 'applications/diffusion/controller/DiffusionBranchTableController.php',
     'DiffusionBranchTableView' => 'applications/diffusion/view/DiffusionBranchTableView.php',
     'DiffusionBrowseController' => 'applications/diffusion/controller/DiffusionBrowseController.php',
     'DiffusionBrowseDirectoryController' => 'applications/diffusion/controller/DiffusionBrowseDirectoryController.php',
     'DiffusionBrowseFileController' => 'applications/diffusion/controller/DiffusionBrowseFileController.php',
     'DiffusionBrowseMainController' => 'applications/diffusion/controller/DiffusionBrowseMainController.php',
     'DiffusionBrowseQueryConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionBrowseQueryConduitAPIMethod.php',
     'DiffusionBrowseResultSet' => 'applications/diffusion/data/DiffusionBrowseResultSet.php',
     'DiffusionBrowseSearchController' => 'applications/diffusion/controller/DiffusionBrowseSearchController.php',
     'DiffusionBrowseTableView' => 'applications/diffusion/view/DiffusionBrowseTableView.php',
     'DiffusionCachedResolveRefsQuery' => 'applications/diffusion/query/DiffusionCachedResolveRefsQuery.php',
     'DiffusionChangeController' => 'applications/diffusion/controller/DiffusionChangeController.php',
     'DiffusionCommitBranchesController' => 'applications/diffusion/controller/DiffusionCommitBranchesController.php',
     'DiffusionCommitChangeTableView' => 'applications/diffusion/view/DiffusionCommitChangeTableView.php',
     'DiffusionCommitController' => 'applications/diffusion/controller/DiffusionCommitController.php',
     'DiffusionCommitEditController' => 'applications/diffusion/controller/DiffusionCommitEditController.php',
     'DiffusionCommitHasRevisionEdgeType' => 'applications/diffusion/edge/DiffusionCommitHasRevisionEdgeType.php',
     'DiffusionCommitHasTaskEdgeType' => 'applications/diffusion/edge/DiffusionCommitHasTaskEdgeType.php',
     'DiffusionCommitHash' => 'applications/diffusion/data/DiffusionCommitHash.php',
     'DiffusionCommitHookEngine' => 'applications/diffusion/engine/DiffusionCommitHookEngine.php',
     'DiffusionCommitHookRejectException' => 'applications/diffusion/exception/DiffusionCommitHookRejectException.php',
     'DiffusionCommitParentsQueryConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionCommitParentsQueryConduitAPIMethod.php',
     'DiffusionCommitQuery' => 'applications/diffusion/query/DiffusionCommitQuery.php',
     'DiffusionCommitRef' => 'applications/diffusion/data/DiffusionCommitRef.php',
     'DiffusionCommitRemarkupRule' => 'applications/diffusion/remarkup/DiffusionCommitRemarkupRule.php',
     'DiffusionCommitRemarkupRuleTestCase' => 'applications/diffusion/remarkup/__tests__/DiffusionCommitRemarkupRuleTestCase.php',
     'DiffusionCommitRevertedByCommitEdgeType' => 'applications/diffusion/edge/DiffusionCommitRevertedByCommitEdgeType.php',
     'DiffusionCommitRevertsCommitEdgeType' => 'applications/diffusion/edge/DiffusionCommitRevertsCommitEdgeType.php',
     'DiffusionCommitTagsController' => 'applications/diffusion/controller/DiffusionCommitTagsController.php',
     'DiffusionConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionConduitAPIMethod.php',
     'DiffusionController' => 'applications/diffusion/controller/DiffusionController.php',
     'DiffusionCreateCommentConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionCreateCommentConduitAPIMethod.php',
     'DiffusionCreateRepositoriesCapability' => 'applications/diffusion/capability/DiffusionCreateRepositoriesCapability.php',
     'DiffusionDefaultEditCapability' => 'applications/diffusion/capability/DiffusionDefaultEditCapability.php',
     'DiffusionDefaultPushCapability' => 'applications/diffusion/capability/DiffusionDefaultPushCapability.php',
     'DiffusionDefaultViewCapability' => 'applications/diffusion/capability/DiffusionDefaultViewCapability.php',
     'DiffusionDiffController' => 'applications/diffusion/controller/DiffusionDiffController.php',
     'DiffusionDiffInlineCommentQuery' => 'applications/diffusion/query/DiffusionDiffInlineCommentQuery.php',
     'DiffusionDiffQueryConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionDiffQueryConduitAPIMethod.php',
     'DiffusionDoorkeeperCommitFeedStoryPublisher' => 'applications/diffusion/doorkeeper/DiffusionDoorkeeperCommitFeedStoryPublisher.php',
     'DiffusionEmptyResultView' => 'applications/diffusion/view/DiffusionEmptyResultView.php',
     'DiffusionExistsQueryConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionExistsQueryConduitAPIMethod.php',
     'DiffusionExternalController' => 'applications/diffusion/controller/DiffusionExternalController.php',
     'DiffusionExternalSymbolQuery' => 'applications/diffusion/symbol/DiffusionExternalSymbolQuery.php',
     'DiffusionExternalSymbolsSource' => 'applications/diffusion/symbol/DiffusionExternalSymbolsSource.php',
     'DiffusionFileContent' => 'applications/diffusion/data/DiffusionFileContent.php',
     'DiffusionFileContentQuery' => 'applications/diffusion/query/filecontent/DiffusionFileContentQuery.php',
     'DiffusionFileContentQueryConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionFileContentQueryConduitAPIMethod.php',
     'DiffusionFindSymbolsConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionFindSymbolsConduitAPIMethod.php',
     'DiffusionGetCommitsConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionGetCommitsConduitAPIMethod.php',
     'DiffusionGetLintMessagesConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionGetLintMessagesConduitAPIMethod.php',
     'DiffusionGetRecentCommitsByPathConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionGetRecentCommitsByPathConduitAPIMethod.php',
     'DiffusionGitBranch' => 'applications/diffusion/data/DiffusionGitBranch.php',
     'DiffusionGitBranchTestCase' => 'applications/diffusion/data/__tests__/DiffusionGitBranchTestCase.php',
     'DiffusionGitFileContentQuery' => 'applications/diffusion/query/filecontent/DiffusionGitFileContentQuery.php',
     'DiffusionGitFileContentQueryTestCase' => 'applications/diffusion/query/__tests__/DiffusionGitFileContentQueryTestCase.php',
     'DiffusionGitRawDiffQuery' => 'applications/diffusion/query/rawdiff/DiffusionGitRawDiffQuery.php',
     'DiffusionGitReceivePackSSHWorkflow' => 'applications/diffusion/ssh/DiffusionGitReceivePackSSHWorkflow.php',
     'DiffusionGitRequest' => 'applications/diffusion/request/DiffusionGitRequest.php',
     'DiffusionGitResponse' => 'applications/diffusion/response/DiffusionGitResponse.php',
     'DiffusionGitSSHWorkflow' => 'applications/diffusion/ssh/DiffusionGitSSHWorkflow.php',
     'DiffusionGitUploadPackSSHWorkflow' => 'applications/diffusion/ssh/DiffusionGitUploadPackSSHWorkflow.php',
     'DiffusionHistoryController' => 'applications/diffusion/controller/DiffusionHistoryController.php',
     'DiffusionHistoryQueryConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionHistoryQueryConduitAPIMethod.php',
     'DiffusionHistoryTableView' => 'applications/diffusion/view/DiffusionHistoryTableView.php',
     'DiffusionHovercardEventListener' => 'applications/diffusion/events/DiffusionHovercardEventListener.php',
     'DiffusionInlineCommentController' => 'applications/diffusion/controller/DiffusionInlineCommentController.php',
     'DiffusionInlineCommentPreviewController' => 'applications/diffusion/controller/DiffusionInlineCommentPreviewController.php',
     'DiffusionLastModifiedController' => 'applications/diffusion/controller/DiffusionLastModifiedController.php',
     'DiffusionLastModifiedQueryConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionLastModifiedQueryConduitAPIMethod.php',
     'DiffusionLintController' => 'applications/diffusion/controller/DiffusionLintController.php',
     'DiffusionLintCountQuery' => 'applications/diffusion/query/DiffusionLintCountQuery.php',
     'DiffusionLintDetailsController' => 'applications/diffusion/controller/DiffusionLintDetailsController.php',
     'DiffusionLintSaveRunner' => 'applications/diffusion/DiffusionLintSaveRunner.php',
     'DiffusionLookSoonConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionLookSoonConduitAPIMethod.php',
     'DiffusionLowLevelCommitFieldsQuery' => 'applications/diffusion/query/lowlevel/DiffusionLowLevelCommitFieldsQuery.php',
     'DiffusionLowLevelCommitQuery' => 'applications/diffusion/query/lowlevel/DiffusionLowLevelCommitQuery.php',
     'DiffusionLowLevelGitRefQuery' => 'applications/diffusion/query/lowlevel/DiffusionLowLevelGitRefQuery.php',
     'DiffusionLowLevelMercurialBranchesQuery' => 'applications/diffusion/query/lowlevel/DiffusionLowLevelMercurialBranchesQuery.php',
     'DiffusionLowLevelMercurialPathsQuery' => 'applications/diffusion/query/lowlevel/DiffusionLowLevelMercurialPathsQuery.php',
     'DiffusionLowLevelParentsQuery' => 'applications/diffusion/query/lowlevel/DiffusionLowLevelParentsQuery.php',
     'DiffusionLowLevelQuery' => 'applications/diffusion/query/lowlevel/DiffusionLowLevelQuery.php',
     'DiffusionLowLevelResolveRefsQuery' => 'applications/diffusion/query/lowlevel/DiffusionLowLevelResolveRefsQuery.php',
     'DiffusionMercurialFileContentQuery' => 'applications/diffusion/query/filecontent/DiffusionMercurialFileContentQuery.php',
     'DiffusionMercurialRawDiffQuery' => 'applications/diffusion/query/rawdiff/DiffusionMercurialRawDiffQuery.php',
     'DiffusionMercurialRequest' => 'applications/diffusion/request/DiffusionMercurialRequest.php',
     'DiffusionMercurialResponse' => 'applications/diffusion/response/DiffusionMercurialResponse.php',
     'DiffusionMercurialSSHWorkflow' => 'applications/diffusion/ssh/DiffusionMercurialSSHWorkflow.php',
     'DiffusionMercurialServeSSHWorkflow' => 'applications/diffusion/ssh/DiffusionMercurialServeSSHWorkflow.php',
     'DiffusionMercurialWireClientSSHProtocolChannel' => 'applications/diffusion/ssh/DiffusionMercurialWireClientSSHProtocolChannel.php',
     'DiffusionMercurialWireProtocol' => 'applications/diffusion/protocol/DiffusionMercurialWireProtocol.php',
     'DiffusionMercurialWireSSHTestCase' => 'applications/diffusion/ssh/__tests__/DiffusionMercurialWireSSHTestCase.php',
     'DiffusionMergedCommitsQueryConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionMergedCommitsQueryConduitAPIMethod.php',
     'DiffusionMirrorDeleteController' => 'applications/diffusion/controller/DiffusionMirrorDeleteController.php',
     'DiffusionMirrorEditController' => 'applications/diffusion/controller/DiffusionMirrorEditController.php',
     'DiffusionPathChange' => 'applications/diffusion/data/DiffusionPathChange.php',
     'DiffusionPathChangeQuery' => 'applications/diffusion/query/pathchange/DiffusionPathChangeQuery.php',
     'DiffusionPathCompleteController' => 'applications/diffusion/controller/DiffusionPathCompleteController.php',
     'DiffusionPathIDQuery' => 'applications/diffusion/query/pathid/DiffusionPathIDQuery.php',
     'DiffusionPathQuery' => 'applications/diffusion/query/DiffusionPathQuery.php',
     'DiffusionPathQueryTestCase' => 'applications/diffusion/query/pathid/__tests__/DiffusionPathQueryTestCase.php',
     'DiffusionPathTreeController' => 'applications/diffusion/controller/DiffusionPathTreeController.php',
     'DiffusionPathValidateController' => 'applications/diffusion/controller/DiffusionPathValidateController.php',
     'DiffusionPhpExternalSymbolsSource' => 'applications/diffusion/symbol/DiffusionPhpExternalSymbolsSource.php',
     'DiffusionPushCapability' => 'applications/diffusion/capability/DiffusionPushCapability.php',
     'DiffusionPushEventViewController' => 'applications/diffusion/controller/DiffusionPushEventViewController.php',
     'DiffusionPushLogController' => 'applications/diffusion/controller/DiffusionPushLogController.php',
     'DiffusionPushLogListController' => 'applications/diffusion/controller/DiffusionPushLogListController.php',
     'DiffusionPushLogListView' => 'applications/diffusion/view/DiffusionPushLogListView.php',
     'DiffusionPythonExternalSymbolsSource' => 'applications/diffusion/symbol/DiffusionPythonExternalSymbolsSource.php',
     'DiffusionQuery' => 'applications/diffusion/query/DiffusionQuery.php',
     'DiffusionQueryCommitsConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionQueryCommitsConduitAPIMethod.php',
     'DiffusionQueryConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionQueryConduitAPIMethod.php',
     'DiffusionQueryPathsConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionQueryPathsConduitAPIMethod.php',
     'DiffusionRawDiffQuery' => 'applications/diffusion/query/rawdiff/DiffusionRawDiffQuery.php',
     'DiffusionRawDiffQueryConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionRawDiffQueryConduitAPIMethod.php',
     'DiffusionReadmeView' => 'applications/diffusion/view/DiffusionReadmeView.php',
     'DiffusionRefNotFoundException' => 'applications/diffusion/exception/DiffusionRefNotFoundException.php',
     'DiffusionRefTableController' => 'applications/diffusion/controller/DiffusionRefTableController.php',
     'DiffusionRefsQueryConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionRefsQueryConduitAPIMethod.php',
     'DiffusionRenameHistoryQuery' => 'applications/diffusion/query/DiffusionRenameHistoryQuery.php',
     'DiffusionRepositoryByIDRemarkupRule' => 'applications/diffusion/remarkup/DiffusionRepositoryByIDRemarkupRule.php',
     'DiffusionRepositoryController' => 'applications/diffusion/controller/DiffusionRepositoryController.php',
     'DiffusionRepositoryCreateController' => 'applications/diffusion/controller/DiffusionRepositoryCreateController.php',
     'DiffusionRepositoryDatasource' => 'applications/diffusion/typeahead/DiffusionRepositoryDatasource.php',
     'DiffusionRepositoryDefaultController' => 'applications/diffusion/controller/DiffusionRepositoryDefaultController.php',
     'DiffusionRepositoryEditActionsController' => 'applications/diffusion/controller/DiffusionRepositoryEditActionsController.php',
     'DiffusionRepositoryEditActivateController' => 'applications/diffusion/controller/DiffusionRepositoryEditActivateController.php',
     'DiffusionRepositoryEditBasicController' => 'applications/diffusion/controller/DiffusionRepositoryEditBasicController.php',
     'DiffusionRepositoryEditBranchesController' => 'applications/diffusion/controller/DiffusionRepositoryEditBranchesController.php',
     'DiffusionRepositoryEditController' => 'applications/diffusion/controller/DiffusionRepositoryEditController.php',
     'DiffusionRepositoryEditDangerousController' => 'applications/diffusion/controller/DiffusionRepositoryEditDangerousController.php',
     'DiffusionRepositoryEditDeleteController' => 'applications/diffusion/controller/DiffusionRepositoryEditDeleteController.php',
     'DiffusionRepositoryEditEncodingController' => 'applications/diffusion/controller/DiffusionRepositoryEditEncodingController.php',
     'DiffusionRepositoryEditHostingController' => 'applications/diffusion/controller/DiffusionRepositoryEditHostingController.php',
     'DiffusionRepositoryEditMainController' => 'applications/diffusion/controller/DiffusionRepositoryEditMainController.php',
     'DiffusionRepositoryEditStagingController' => 'applications/diffusion/controller/DiffusionRepositoryEditStagingController.php',
     'DiffusionRepositoryEditStorageController' => 'applications/diffusion/controller/DiffusionRepositoryEditStorageController.php',
     'DiffusionRepositoryEditSubversionController' => 'applications/diffusion/controller/DiffusionRepositoryEditSubversionController.php',
     'DiffusionRepositoryEditUpdateController' => 'applications/diffusion/controller/DiffusionRepositoryEditUpdateController.php',
     'DiffusionRepositoryListController' => 'applications/diffusion/controller/DiffusionRepositoryListController.php',
     'DiffusionRepositoryNewController' => 'applications/diffusion/controller/DiffusionRepositoryNewController.php',
     'DiffusionRepositoryPath' => 'applications/diffusion/data/DiffusionRepositoryPath.php',
     'DiffusionRepositoryRef' => 'applications/diffusion/data/DiffusionRepositoryRef.php',
     'DiffusionRepositoryRemarkupRule' => 'applications/diffusion/remarkup/DiffusionRepositoryRemarkupRule.php',
     'DiffusionRepositorySymbolsController' => 'applications/diffusion/controller/DiffusionRepositorySymbolsController.php',
     'DiffusionRepositoryTag' => 'applications/diffusion/data/DiffusionRepositoryTag.php',
     'DiffusionRequest' => 'applications/diffusion/request/DiffusionRequest.php',
     'DiffusionResolveRefsConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionResolveRefsConduitAPIMethod.php',
     'DiffusionResolveUserQuery' => 'applications/diffusion/query/DiffusionResolveUserQuery.php',
     'DiffusionSSHWorkflow' => 'applications/diffusion/ssh/DiffusionSSHWorkflow.php',
     'DiffusionSearchQueryConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionSearchQueryConduitAPIMethod.php',
     'DiffusionServeController' => 'applications/diffusion/controller/DiffusionServeController.php',
     'DiffusionSetPasswordSettingsPanel' => 'applications/diffusion/panel/DiffusionSetPasswordSettingsPanel.php',
     'DiffusionSetupException' => 'applications/diffusion/exception/DiffusionSetupException.php',
     'DiffusionSubversionSSHWorkflow' => 'applications/diffusion/ssh/DiffusionSubversionSSHWorkflow.php',
     'DiffusionSubversionServeSSHWorkflow' => 'applications/diffusion/ssh/DiffusionSubversionServeSSHWorkflow.php',
     'DiffusionSubversionWireProtocol' => 'applications/diffusion/protocol/DiffusionSubversionWireProtocol.php',
     'DiffusionSubversionWireProtocolTestCase' => 'applications/diffusion/protocol/__tests__/DiffusionSubversionWireProtocolTestCase.php',
     'DiffusionSvnFileContentQuery' => 'applications/diffusion/query/filecontent/DiffusionSvnFileContentQuery.php',
     'DiffusionSvnRawDiffQuery' => 'applications/diffusion/query/rawdiff/DiffusionSvnRawDiffQuery.php',
     'DiffusionSvnRequest' => 'applications/diffusion/request/DiffusionSvnRequest.php',
     'DiffusionSymbolController' => 'applications/diffusion/controller/DiffusionSymbolController.php',
     'DiffusionSymbolDatasource' => 'applications/diffusion/typeahead/DiffusionSymbolDatasource.php',
     'DiffusionSymbolQuery' => 'applications/diffusion/query/DiffusionSymbolQuery.php',
     'DiffusionTagListController' => 'applications/diffusion/controller/DiffusionTagListController.php',
     'DiffusionTagListView' => 'applications/diffusion/view/DiffusionTagListView.php',
     'DiffusionTagsQueryConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionTagsQueryConduitAPIMethod.php',
     'DiffusionURITestCase' => 'applications/diffusion/request/__tests__/DiffusionURITestCase.php',
     'DiffusionUpdateCoverageConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionUpdateCoverageConduitAPIMethod.php',
     'DiffusionView' => 'applications/diffusion/view/DiffusionView.php',
     'DivinerArticleAtomizer' => 'applications/diviner/atomizer/DivinerArticleAtomizer.php',
     'DivinerAtom' => 'applications/diviner/atom/DivinerAtom.php',
     'DivinerAtomCache' => 'applications/diviner/cache/DivinerAtomCache.php',
     'DivinerAtomController' => 'applications/diviner/controller/DivinerAtomController.php',
     'DivinerAtomListController' => 'applications/diviner/controller/DivinerAtomListController.php',
     'DivinerAtomPHIDType' => 'applications/diviner/phid/DivinerAtomPHIDType.php',
     'DivinerAtomQuery' => 'applications/diviner/query/DivinerAtomQuery.php',
     'DivinerAtomRef' => 'applications/diviner/atom/DivinerAtomRef.php',
     'DivinerAtomSearchEngine' => 'applications/diviner/query/DivinerAtomSearchEngine.php',
     'DivinerAtomSearchIndexer' => 'applications/diviner/search/DivinerAtomSearchIndexer.php',
     'DivinerAtomizeWorkflow' => 'applications/diviner/workflow/DivinerAtomizeWorkflow.php',
     'DivinerAtomizer' => 'applications/diviner/atomizer/DivinerAtomizer.php',
     'DivinerBookController' => 'applications/diviner/controller/DivinerBookController.php',
     'DivinerBookItemView' => 'applications/diviner/view/DivinerBookItemView.php',
     'DivinerBookPHIDType' => 'applications/diviner/phid/DivinerBookPHIDType.php',
     'DivinerBookQuery' => 'applications/diviner/query/DivinerBookQuery.php',
     'DivinerBookSearchIndexer' => 'applications/diviner/search/DivinerBookSearchIndexer.php',
     'DivinerController' => 'applications/diviner/controller/DivinerController.php',
     'DivinerDAO' => 'applications/diviner/storage/DivinerDAO.php',
     'DivinerDefaultRenderer' => 'applications/diviner/renderer/DivinerDefaultRenderer.php',
     'DivinerDiskCache' => 'applications/diviner/cache/DivinerDiskCache.php',
     'DivinerFileAtomizer' => 'applications/diviner/atomizer/DivinerFileAtomizer.php',
     'DivinerFindController' => 'applications/diviner/controller/DivinerFindController.php',
     'DivinerGenerateWorkflow' => 'applications/diviner/workflow/DivinerGenerateWorkflow.php',
     'DivinerLiveAtom' => 'applications/diviner/storage/DivinerLiveAtom.php',
     'DivinerLiveBook' => 'applications/diviner/storage/DivinerLiveBook.php',
     'DivinerLivePublisher' => 'applications/diviner/publisher/DivinerLivePublisher.php',
     'DivinerLiveSymbol' => 'applications/diviner/storage/DivinerLiveSymbol.php',
     'DivinerMainController' => 'applications/diviner/controller/DivinerMainController.php',
     'DivinerPHPAtomizer' => 'applications/diviner/atomizer/DivinerPHPAtomizer.php',
     'DivinerParameterTableView' => 'applications/diviner/view/DivinerParameterTableView.php',
     'DivinerPublishCache' => 'applications/diviner/cache/DivinerPublishCache.php',
     'DivinerPublisher' => 'applications/diviner/publisher/DivinerPublisher.php',
     'DivinerRenderer' => 'applications/diviner/renderer/DivinerRenderer.php',
     'DivinerReturnTableView' => 'applications/diviner/view/DivinerReturnTableView.php',
     'DivinerSectionView' => 'applications/diviner/view/DivinerSectionView.php',
     'DivinerStaticPublisher' => 'applications/diviner/publisher/DivinerStaticPublisher.php',
     'DivinerSymbolRemarkupRule' => 'applications/diviner/markup/DivinerSymbolRemarkupRule.php',
     'DivinerWorkflow' => 'applications/diviner/workflow/DivinerWorkflow.php',
     'DoorkeeperAsanaFeedWorker' => 'applications/doorkeeper/worker/DoorkeeperAsanaFeedWorker.php',
     'DoorkeeperAsanaRemarkupRule' => 'applications/doorkeeper/remarkup/DoorkeeperAsanaRemarkupRule.php',
     'DoorkeeperBridge' => 'applications/doorkeeper/bridge/DoorkeeperBridge.php',
     'DoorkeeperBridgeAsana' => 'applications/doorkeeper/bridge/DoorkeeperBridgeAsana.php',
     'DoorkeeperBridgeJIRA' => 'applications/doorkeeper/bridge/DoorkeeperBridgeJIRA.php',
     'DoorkeeperBridgeJIRATestCase' => 'applications/doorkeeper/bridge/__tests__/DoorkeeperBridgeJIRATestCase.php',
     'DoorkeeperDAO' => 'applications/doorkeeper/storage/DoorkeeperDAO.php',
     'DoorkeeperExternalObject' => 'applications/doorkeeper/storage/DoorkeeperExternalObject.php',
     'DoorkeeperExternalObjectQuery' => 'applications/doorkeeper/query/DoorkeeperExternalObjectQuery.php',
     'DoorkeeperFeedStoryPublisher' => 'applications/doorkeeper/engine/DoorkeeperFeedStoryPublisher.php',
     'DoorkeeperFeedWorker' => 'applications/doorkeeper/worker/DoorkeeperFeedWorker.php',
     'DoorkeeperImportEngine' => 'applications/doorkeeper/engine/DoorkeeperImportEngine.php',
     'DoorkeeperJIRAFeedWorker' => 'applications/doorkeeper/worker/DoorkeeperJIRAFeedWorker.php',
     'DoorkeeperJIRARemarkupRule' => 'applications/doorkeeper/remarkup/DoorkeeperJIRARemarkupRule.php',
     'DoorkeeperMissingLinkException' => 'applications/doorkeeper/exception/DoorkeeperMissingLinkException.php',
     'DoorkeeperObjectRef' => 'applications/doorkeeper/engine/DoorkeeperObjectRef.php',
     'DoorkeeperRemarkupRule' => 'applications/doorkeeper/remarkup/DoorkeeperRemarkupRule.php',
     'DoorkeeperSchemaSpec' => 'applications/doorkeeper/storage/DoorkeeperSchemaSpec.php',
     'DoorkeeperTagView' => 'applications/doorkeeper/view/DoorkeeperTagView.php',
     'DoorkeeperTagsController' => 'applications/doorkeeper/controller/DoorkeeperTagsController.php',
     'DrydockAllocatorWorker' => 'applications/drydock/worker/DrydockAllocatorWorker.php',
     'DrydockApacheWebrootInterface' => 'applications/drydock/interface/webroot/DrydockApacheWebrootInterface.php',
     'DrydockBlueprint' => 'applications/drydock/storage/DrydockBlueprint.php',
     'DrydockBlueprintController' => 'applications/drydock/controller/DrydockBlueprintController.php',
     'DrydockBlueprintCoreCustomField' => 'applications/drydock/customfield/DrydockBlueprintCoreCustomField.php',
     'DrydockBlueprintCreateController' => 'applications/drydock/controller/DrydockBlueprintCreateController.php',
     'DrydockBlueprintCustomField' => 'applications/drydock/customfield/DrydockBlueprintCustomField.php',
     'DrydockBlueprintEditController' => 'applications/drydock/controller/DrydockBlueprintEditController.php',
     'DrydockBlueprintEditor' => 'applications/drydock/editor/DrydockBlueprintEditor.php',
     'DrydockBlueprintImplementation' => 'applications/drydock/blueprint/DrydockBlueprintImplementation.php',
+    'DrydockBlueprintImplementationTestCase' => 'applications/drydock/blueprint/__tests__/DrydockBlueprintImplementationTestCase.php',
     'DrydockBlueprintListController' => 'applications/drydock/controller/DrydockBlueprintListController.php',
     'DrydockBlueprintPHIDType' => 'applications/drydock/phid/DrydockBlueprintPHIDType.php',
     'DrydockBlueprintQuery' => 'applications/drydock/query/DrydockBlueprintQuery.php',
     'DrydockBlueprintScopeGuard' => 'applications/drydock/util/DrydockBlueprintScopeGuard.php',
     'DrydockBlueprintSearchEngine' => 'applications/drydock/query/DrydockBlueprintSearchEngine.php',
     'DrydockBlueprintTransaction' => 'applications/drydock/storage/DrydockBlueprintTransaction.php',
     'DrydockBlueprintTransactionQuery' => 'applications/drydock/query/DrydockBlueprintTransactionQuery.php',
     'DrydockBlueprintViewController' => 'applications/drydock/controller/DrydockBlueprintViewController.php',
     'DrydockCommandInterface' => 'applications/drydock/interface/command/DrydockCommandInterface.php',
     'DrydockConsoleController' => 'applications/drydock/controller/DrydockConsoleController.php',
     'DrydockConstants' => 'applications/drydock/constants/DrydockConstants.php',
     'DrydockController' => 'applications/drydock/controller/DrydockController.php',
     'DrydockCreateBlueprintsCapability' => 'applications/drydock/capability/DrydockCreateBlueprintsCapability.php',
     'DrydockDAO' => 'applications/drydock/storage/DrydockDAO.php',
     'DrydockDefaultEditCapability' => 'applications/drydock/capability/DrydockDefaultEditCapability.php',
     'DrydockDefaultViewCapability' => 'applications/drydock/capability/DrydockDefaultViewCapability.php',
     'DrydockFilesystemInterface' => 'applications/drydock/interface/filesystem/DrydockFilesystemInterface.php',
     'DrydockInterface' => 'applications/drydock/interface/DrydockInterface.php',
     'DrydockLease' => 'applications/drydock/storage/DrydockLease.php',
     'DrydockLeaseController' => 'applications/drydock/controller/DrydockLeaseController.php',
     'DrydockLeaseListController' => 'applications/drydock/controller/DrydockLeaseListController.php',
     'DrydockLeaseListView' => 'applications/drydock/view/DrydockLeaseListView.php',
     'DrydockLeasePHIDType' => 'applications/drydock/phid/DrydockLeasePHIDType.php',
     'DrydockLeaseQuery' => 'applications/drydock/query/DrydockLeaseQuery.php',
     'DrydockLeaseReleaseController' => 'applications/drydock/controller/DrydockLeaseReleaseController.php',
     'DrydockLeaseSearchEngine' => 'applications/drydock/query/DrydockLeaseSearchEngine.php',
     'DrydockLeaseStatus' => 'applications/drydock/constants/DrydockLeaseStatus.php',
     'DrydockLeaseViewController' => 'applications/drydock/controller/DrydockLeaseViewController.php',
     'DrydockLocalCommandInterface' => 'applications/drydock/interface/command/DrydockLocalCommandInterface.php',
     'DrydockLog' => 'applications/drydock/storage/DrydockLog.php',
     'DrydockLogController' => 'applications/drydock/controller/DrydockLogController.php',
     'DrydockLogListController' => 'applications/drydock/controller/DrydockLogListController.php',
     'DrydockLogListView' => 'applications/drydock/view/DrydockLogListView.php',
     'DrydockLogQuery' => 'applications/drydock/query/DrydockLogQuery.php',
     'DrydockLogSearchEngine' => 'applications/drydock/query/DrydockLogSearchEngine.php',
     'DrydockManagementCloseWorkflow' => 'applications/drydock/management/DrydockManagementCloseWorkflow.php',
     'DrydockManagementCreateResourceWorkflow' => 'applications/drydock/management/DrydockManagementCreateResourceWorkflow.php',
     'DrydockManagementLeaseWorkflow' => 'applications/drydock/management/DrydockManagementLeaseWorkflow.php',
     'DrydockManagementReleaseWorkflow' => 'applications/drydock/management/DrydockManagementReleaseWorkflow.php',
     'DrydockManagementWorkflow' => 'applications/drydock/management/DrydockManagementWorkflow.php',
     'DrydockPreallocatedHostBlueprintImplementation' => 'applications/drydock/blueprint/DrydockPreallocatedHostBlueprintImplementation.php',
     'DrydockQuery' => 'applications/drydock/query/DrydockQuery.php',
     'DrydockResource' => 'applications/drydock/storage/DrydockResource.php',
     'DrydockResourceCloseController' => 'applications/drydock/controller/DrydockResourceCloseController.php',
     'DrydockResourceController' => 'applications/drydock/controller/DrydockResourceController.php',
     'DrydockResourceListController' => 'applications/drydock/controller/DrydockResourceListController.php',
     'DrydockResourceListView' => 'applications/drydock/view/DrydockResourceListView.php',
     'DrydockResourcePHIDType' => 'applications/drydock/phid/DrydockResourcePHIDType.php',
     'DrydockResourceQuery' => 'applications/drydock/query/DrydockResourceQuery.php',
     'DrydockResourceSearchEngine' => 'applications/drydock/query/DrydockResourceSearchEngine.php',
     'DrydockResourceStatus' => 'applications/drydock/constants/DrydockResourceStatus.php',
     'DrydockResourceViewController' => 'applications/drydock/controller/DrydockResourceViewController.php',
     'DrydockSFTPFilesystemInterface' => 'applications/drydock/interface/filesystem/DrydockSFTPFilesystemInterface.php',
     'DrydockSSHCommandInterface' => 'applications/drydock/interface/command/DrydockSSHCommandInterface.php',
     'DrydockWebrootInterface' => 'applications/drydock/interface/webroot/DrydockWebrootInterface.php',
     'DrydockWorkingCopyBlueprintImplementation' => 'applications/drydock/blueprint/DrydockWorkingCopyBlueprintImplementation.php',
     'FeedConduitAPIMethod' => 'applications/feed/conduit/FeedConduitAPIMethod.php',
     'FeedPublishConduitAPIMethod' => 'applications/feed/conduit/FeedPublishConduitAPIMethod.php',
     'FeedPublisherHTTPWorker' => 'applications/feed/worker/FeedPublisherHTTPWorker.php',
     'FeedPublisherWorker' => 'applications/feed/worker/FeedPublisherWorker.php',
     'FeedPushWorker' => 'applications/feed/worker/FeedPushWorker.php',
     'FeedQueryConduitAPIMethod' => 'applications/feed/conduit/FeedQueryConduitAPIMethod.php',
+    'FeedStoryNotificationGarbageCollector' => 'applications/notification/garbagecollector/FeedStoryNotificationGarbageCollector.php',
     'FileAllocateConduitAPIMethod' => 'applications/files/conduit/FileAllocateConduitAPIMethod.php',
     'FileConduitAPIMethod' => 'applications/files/conduit/FileConduitAPIMethod.php',
     'FileCreateMailReceiver' => 'applications/files/mail/FileCreateMailReceiver.php',
     'FileDownloadConduitAPIMethod' => 'applications/files/conduit/FileDownloadConduitAPIMethod.php',
     'FileInfoConduitAPIMethod' => 'applications/files/conduit/FileInfoConduitAPIMethod.php',
     'FileMailReceiver' => 'applications/files/mail/FileMailReceiver.php',
     'FileQueryChunksConduitAPIMethod' => 'applications/files/conduit/FileQueryChunksConduitAPIMethod.php',
     'FileReplyHandler' => 'applications/files/mail/FileReplyHandler.php',
     'FileUploadChunkConduitAPIMethod' => 'applications/files/conduit/FileUploadChunkConduitAPIMethod.php',
     'FileUploadConduitAPIMethod' => 'applications/files/conduit/FileUploadConduitAPIMethod.php',
     'FileUploadHashConduitAPIMethod' => 'applications/files/conduit/FileUploadHashConduitAPIMethod.php',
     'FilesDefaultViewCapability' => 'applications/files/capability/FilesDefaultViewCapability.php',
     'FlagConduitAPIMethod' => 'applications/flag/conduit/FlagConduitAPIMethod.php',
     'FlagDeleteConduitAPIMethod' => 'applications/flag/conduit/FlagDeleteConduitAPIMethod.php',
     'FlagEditConduitAPIMethod' => 'applications/flag/conduit/FlagEditConduitAPIMethod.php',
     'FlagQueryConduitAPIMethod' => 'applications/flag/conduit/FlagQueryConduitAPIMethod.php',
     'FundBacker' => 'applications/fund/storage/FundBacker.php',
     'FundBackerCart' => 'applications/fund/phortune/FundBackerCart.php',
     'FundBackerEditor' => 'applications/fund/editor/FundBackerEditor.php',
     'FundBackerListController' => 'applications/fund/controller/FundBackerListController.php',
     'FundBackerPHIDType' => 'applications/fund/phid/FundBackerPHIDType.php',
     'FundBackerProduct' => 'applications/fund/phortune/FundBackerProduct.php',
     'FundBackerQuery' => 'applications/fund/query/FundBackerQuery.php',
     'FundBackerSearchEngine' => 'applications/fund/query/FundBackerSearchEngine.php',
     'FundBackerTransaction' => 'applications/fund/storage/FundBackerTransaction.php',
     'FundBackerTransactionQuery' => 'applications/fund/query/FundBackerTransactionQuery.php',
     'FundController' => 'applications/fund/controller/FundController.php',
     'FundCreateInitiativesCapability' => 'applications/fund/capability/FundCreateInitiativesCapability.php',
     'FundDAO' => 'applications/fund/storage/FundDAO.php',
     'FundDefaultViewCapability' => 'applications/fund/capability/FundDefaultViewCapability.php',
     'FundInitiative' => 'applications/fund/storage/FundInitiative.php',
     'FundInitiativeBackController' => 'applications/fund/controller/FundInitiativeBackController.php',
     'FundInitiativeCloseController' => 'applications/fund/controller/FundInitiativeCloseController.php',
     'FundInitiativeEditController' => 'applications/fund/controller/FundInitiativeEditController.php',
     'FundInitiativeEditor' => 'applications/fund/editor/FundInitiativeEditor.php',
     'FundInitiativeIndexer' => 'applications/fund/search/FundInitiativeIndexer.php',
     'FundInitiativeListController' => 'applications/fund/controller/FundInitiativeListController.php',
     'FundInitiativePHIDType' => 'applications/fund/phid/FundInitiativePHIDType.php',
     'FundInitiativeQuery' => 'applications/fund/query/FundInitiativeQuery.php',
     'FundInitiativeRemarkupRule' => 'applications/fund/remarkup/FundInitiativeRemarkupRule.php',
     'FundInitiativeReplyHandler' => 'applications/fund/mail/FundInitiativeReplyHandler.php',
     'FundInitiativeSearchEngine' => 'applications/fund/query/FundInitiativeSearchEngine.php',
     'FundInitiativeTransaction' => 'applications/fund/storage/FundInitiativeTransaction.php',
     'FundInitiativeTransactionQuery' => 'applications/fund/query/FundInitiativeTransactionQuery.php',
     'FundInitiativeViewController' => 'applications/fund/controller/FundInitiativeViewController.php',
     'FundSchemaSpec' => 'applications/fund/storage/FundSchemaSpec.php',
     'HarbormasterBuild' => 'applications/harbormaster/storage/build/HarbormasterBuild.php',
     'HarbormasterBuildAbortedException' => 'applications/harbormaster/exception/HarbormasterBuildAbortedException.php',
     'HarbormasterBuildActionController' => 'applications/harbormaster/controller/HarbormasterBuildActionController.php',
     'HarbormasterBuildArtifact' => 'applications/harbormaster/storage/build/HarbormasterBuildArtifact.php',
     'HarbormasterBuildArtifactQuery' => 'applications/harbormaster/query/HarbormasterBuildArtifactQuery.php',
     'HarbormasterBuildCommand' => 'applications/harbormaster/storage/HarbormasterBuildCommand.php',
     'HarbormasterBuildDependencyDatasource' => 'applications/harbormaster/typeahead/HarbormasterBuildDependencyDatasource.php',
     'HarbormasterBuildEngine' => 'applications/harbormaster/engine/HarbormasterBuildEngine.php',
     'HarbormasterBuildFailureException' => 'applications/harbormaster/exception/HarbormasterBuildFailureException.php',
     'HarbormasterBuildGraph' => 'applications/harbormaster/engine/HarbormasterBuildGraph.php',
     'HarbormasterBuildItem' => 'applications/harbormaster/storage/build/HarbormasterBuildItem.php',
     'HarbormasterBuildItemPHIDType' => 'applications/harbormaster/phid/HarbormasterBuildItemPHIDType.php',
     'HarbormasterBuildItemQuery' => 'applications/harbormaster/query/HarbormasterBuildItemQuery.php',
     'HarbormasterBuildLog' => 'applications/harbormaster/storage/build/HarbormasterBuildLog.php',
     'HarbormasterBuildLogPHIDType' => 'applications/harbormaster/phid/HarbormasterBuildLogPHIDType.php',
     'HarbormasterBuildLogQuery' => 'applications/harbormaster/query/HarbormasterBuildLogQuery.php',
     'HarbormasterBuildMessage' => 'applications/harbormaster/storage/HarbormasterBuildMessage.php',
     'HarbormasterBuildMessageQuery' => 'applications/harbormaster/query/HarbormasterBuildMessageQuery.php',
     'HarbormasterBuildPHIDType' => 'applications/harbormaster/phid/HarbormasterBuildPHIDType.php',
     'HarbormasterBuildPlan' => 'applications/harbormaster/storage/configuration/HarbormasterBuildPlan.php',
     'HarbormasterBuildPlanDatasource' => 'applications/harbormaster/typeahead/HarbormasterBuildPlanDatasource.php',
     'HarbormasterBuildPlanEditor' => 'applications/harbormaster/editor/HarbormasterBuildPlanEditor.php',
     'HarbormasterBuildPlanPHIDType' => 'applications/harbormaster/phid/HarbormasterBuildPlanPHIDType.php',
     'HarbormasterBuildPlanQuery' => 'applications/harbormaster/query/HarbormasterBuildPlanQuery.php',
     'HarbormasterBuildPlanSearchEngine' => 'applications/harbormaster/query/HarbormasterBuildPlanSearchEngine.php',
     'HarbormasterBuildPlanTransaction' => 'applications/harbormaster/storage/configuration/HarbormasterBuildPlanTransaction.php',
     'HarbormasterBuildPlanTransactionQuery' => 'applications/harbormaster/query/HarbormasterBuildPlanTransactionQuery.php',
     'HarbormasterBuildQuery' => 'applications/harbormaster/query/HarbormasterBuildQuery.php',
     'HarbormasterBuildStep' => 'applications/harbormaster/storage/configuration/HarbormasterBuildStep.php',
     'HarbormasterBuildStepCoreCustomField' => 'applications/harbormaster/customfield/HarbormasterBuildStepCoreCustomField.php',
     'HarbormasterBuildStepCustomField' => 'applications/harbormaster/customfield/HarbormasterBuildStepCustomField.php',
     'HarbormasterBuildStepEditor' => 'applications/harbormaster/editor/HarbormasterBuildStepEditor.php',
     'HarbormasterBuildStepImplementation' => 'applications/harbormaster/step/HarbormasterBuildStepImplementation.php',
+    'HarbormasterBuildStepImplementationTestCase' => 'applications/harbormaster/step/__tests__/HarbormasterBuildStepImplementationTestCase.php',
     'HarbormasterBuildStepPHIDType' => 'applications/harbormaster/phid/HarbormasterBuildStepPHIDType.php',
     'HarbormasterBuildStepQuery' => 'applications/harbormaster/query/HarbormasterBuildStepQuery.php',
     'HarbormasterBuildStepTransaction' => 'applications/harbormaster/storage/configuration/HarbormasterBuildStepTransaction.php',
     'HarbormasterBuildStepTransactionQuery' => 'applications/harbormaster/query/HarbormasterBuildStepTransactionQuery.php',
     'HarbormasterBuildTarget' => 'applications/harbormaster/storage/build/HarbormasterBuildTarget.php',
     'HarbormasterBuildTargetPHIDType' => 'applications/harbormaster/phid/HarbormasterBuildTargetPHIDType.php',
     'HarbormasterBuildTargetQuery' => 'applications/harbormaster/query/HarbormasterBuildTargetQuery.php',
     'HarbormasterBuildTransaction' => 'applications/harbormaster/storage/HarbormasterBuildTransaction.php',
     'HarbormasterBuildTransactionEditor' => 'applications/harbormaster/editor/HarbormasterBuildTransactionEditor.php',
     'HarbormasterBuildTransactionQuery' => 'applications/harbormaster/query/HarbormasterBuildTransactionQuery.php',
     'HarbormasterBuildViewController' => 'applications/harbormaster/controller/HarbormasterBuildViewController.php',
     'HarbormasterBuildWorker' => 'applications/harbormaster/worker/HarbormasterBuildWorker.php',
     'HarbormasterBuildable' => 'applications/harbormaster/storage/HarbormasterBuildable.php',
     'HarbormasterBuildableActionController' => 'applications/harbormaster/controller/HarbormasterBuildableActionController.php',
     'HarbormasterBuildableInterface' => 'applications/harbormaster/interface/HarbormasterBuildableInterface.php',
     'HarbormasterBuildableListController' => 'applications/harbormaster/controller/HarbormasterBuildableListController.php',
     'HarbormasterBuildablePHIDType' => 'applications/harbormaster/phid/HarbormasterBuildablePHIDType.php',
     'HarbormasterBuildableQuery' => 'applications/harbormaster/query/HarbormasterBuildableQuery.php',
     'HarbormasterBuildableSearchEngine' => 'applications/harbormaster/query/HarbormasterBuildableSearchEngine.php',
     'HarbormasterBuildableTransaction' => 'applications/harbormaster/storage/HarbormasterBuildableTransaction.php',
     'HarbormasterBuildableTransactionEditor' => 'applications/harbormaster/editor/HarbormasterBuildableTransactionEditor.php',
     'HarbormasterBuildableTransactionQuery' => 'applications/harbormaster/query/HarbormasterBuildableTransactionQuery.php',
     'HarbormasterBuildableViewController' => 'applications/harbormaster/controller/HarbormasterBuildableViewController.php',
     'HarbormasterCommandBuildStepImplementation' => 'applications/harbormaster/step/HarbormasterCommandBuildStepImplementation.php',
     'HarbormasterConduitAPIMethod' => 'applications/harbormaster/conduit/HarbormasterConduitAPIMethod.php',
     'HarbormasterController' => 'applications/harbormaster/controller/HarbormasterController.php',
     'HarbormasterDAO' => 'applications/harbormaster/storage/HarbormasterDAO.php',
     'HarbormasterHTTPRequestBuildStepImplementation' => 'applications/harbormaster/step/HarbormasterHTTPRequestBuildStepImplementation.php',
     'HarbormasterLeaseHostBuildStepImplementation' => 'applications/harbormaster/step/HarbormasterLeaseHostBuildStepImplementation.php',
     'HarbormasterManagePlansCapability' => 'applications/harbormaster/capability/HarbormasterManagePlansCapability.php',
     'HarbormasterManagementBuildWorkflow' => 'applications/harbormaster/management/HarbormasterManagementBuildWorkflow.php',
     'HarbormasterManagementUpdateWorkflow' => 'applications/harbormaster/management/HarbormasterManagementUpdateWorkflow.php',
     'HarbormasterManagementWorkflow' => 'applications/harbormaster/management/HarbormasterManagementWorkflow.php',
     'HarbormasterObject' => 'applications/harbormaster/storage/HarbormasterObject.php',
     'HarbormasterPlanController' => 'applications/harbormaster/controller/HarbormasterPlanController.php',
     'HarbormasterPlanDisableController' => 'applications/harbormaster/controller/HarbormasterPlanDisableController.php',
     'HarbormasterPlanEditController' => 'applications/harbormaster/controller/HarbormasterPlanEditController.php',
     'HarbormasterPlanListController' => 'applications/harbormaster/controller/HarbormasterPlanListController.php',
     'HarbormasterPlanRunController' => 'applications/harbormaster/controller/HarbormasterPlanRunController.php',
     'HarbormasterPlanViewController' => 'applications/harbormaster/controller/HarbormasterPlanViewController.php',
     'HarbormasterPublishFragmentBuildStepImplementation' => 'applications/harbormaster/step/HarbormasterPublishFragmentBuildStepImplementation.php',
     'HarbormasterQueryBuildablesConduitAPIMethod' => 'applications/harbormaster/conduit/HarbormasterQueryBuildablesConduitAPIMethod.php',
     'HarbormasterQueryBuildsConduitAPIMethod' => 'applications/harbormaster/conduit/HarbormasterQueryBuildsConduitAPIMethod.php',
     'HarbormasterRemarkupRule' => 'applications/harbormaster/remarkup/HarbormasterRemarkupRule.php',
     'HarbormasterSchemaSpec' => 'applications/harbormaster/storage/HarbormasterSchemaSpec.php',
     'HarbormasterScratchTable' => 'applications/harbormaster/storage/HarbormasterScratchTable.php',
     'HarbormasterSendMessageConduitAPIMethod' => 'applications/harbormaster/conduit/HarbormasterSendMessageConduitAPIMethod.php',
     'HarbormasterSleepBuildStepImplementation' => 'applications/harbormaster/step/HarbormasterSleepBuildStepImplementation.php',
     'HarbormasterStepAddController' => 'applications/harbormaster/controller/HarbormasterStepAddController.php',
     'HarbormasterStepDeleteController' => 'applications/harbormaster/controller/HarbormasterStepDeleteController.php',
     'HarbormasterStepEditController' => 'applications/harbormaster/controller/HarbormasterStepEditController.php',
     'HarbormasterTargetWorker' => 'applications/harbormaster/worker/HarbormasterTargetWorker.php',
     'HarbormasterThrowExceptionBuildStep' => 'applications/harbormaster/step/HarbormasterThrowExceptionBuildStep.php',
     'HarbormasterUIEventListener' => 'applications/harbormaster/event/HarbormasterUIEventListener.php',
     'HarbormasterUploadArtifactBuildStepImplementation' => 'applications/harbormaster/step/HarbormasterUploadArtifactBuildStepImplementation.php',
     'HarbormasterWaitForPreviousBuildStepImplementation' => 'applications/harbormaster/step/HarbormasterWaitForPreviousBuildStepImplementation.php',
     'HarbormasterWorker' => 'applications/harbormaster/worker/HarbormasterWorker.php',
     'HeraldAction' => 'applications/herald/storage/HeraldAction.php',
     'HeraldAdapter' => 'applications/herald/adapter/HeraldAdapter.php',
     'HeraldApplyTranscript' => 'applications/herald/storage/transcript/HeraldApplyTranscript.php',
     'HeraldCommitAdapter' => 'applications/herald/adapter/HeraldCommitAdapter.php',
     'HeraldCondition' => 'applications/herald/storage/HeraldCondition.php',
     'HeraldConditionTranscript' => 'applications/herald/storage/transcript/HeraldConditionTranscript.php',
     'HeraldController' => 'applications/herald/controller/HeraldController.php',
     'HeraldCustomAction' => 'applications/herald/extension/HeraldCustomAction.php',
     'HeraldDAO' => 'applications/herald/storage/HeraldDAO.php',
     'HeraldDifferentialAdapter' => 'applications/herald/adapter/HeraldDifferentialAdapter.php',
     'HeraldDifferentialDiffAdapter' => 'applications/herald/adapter/HeraldDifferentialDiffAdapter.php',
     'HeraldDifferentialRevisionAdapter' => 'applications/herald/adapter/HeraldDifferentialRevisionAdapter.php',
     'HeraldDisableController' => 'applications/herald/controller/HeraldDisableController.php',
     'HeraldEffect' => 'applications/herald/engine/HeraldEffect.php',
     'HeraldEngine' => 'applications/herald/engine/HeraldEngine.php',
     'HeraldInvalidActionException' => 'applications/herald/engine/exception/HeraldInvalidActionException.php',
     'HeraldInvalidConditionException' => 'applications/herald/engine/exception/HeraldInvalidConditionException.php',
     'HeraldManageGlobalRulesCapability' => 'applications/herald/capability/HeraldManageGlobalRulesCapability.php',
     'HeraldManiphestTaskAdapter' => 'applications/herald/adapter/HeraldManiphestTaskAdapter.php',
     'HeraldNewController' => 'applications/herald/controller/HeraldNewController.php',
     'HeraldObjectTranscript' => 'applications/herald/storage/transcript/HeraldObjectTranscript.php',
     'HeraldPholioMockAdapter' => 'applications/herald/adapter/HeraldPholioMockAdapter.php',
     'HeraldPreCommitAdapter' => 'applications/diffusion/herald/HeraldPreCommitAdapter.php',
     'HeraldPreCommitContentAdapter' => 'applications/diffusion/herald/HeraldPreCommitContentAdapter.php',
     'HeraldPreCommitRefAdapter' => 'applications/diffusion/herald/HeraldPreCommitRefAdapter.php',
     'HeraldRecursiveConditionsException' => 'applications/herald/engine/exception/HeraldRecursiveConditionsException.php',
     'HeraldRemarkupRule' => 'applications/herald/remarkup/HeraldRemarkupRule.php',
     'HeraldRepetitionPolicyConfig' => 'applications/herald/config/HeraldRepetitionPolicyConfig.php',
     'HeraldRule' => 'applications/herald/storage/HeraldRule.php',
     'HeraldRuleController' => 'applications/herald/controller/HeraldRuleController.php',
     'HeraldRuleEditor' => 'applications/herald/editor/HeraldRuleEditor.php',
     'HeraldRuleListController' => 'applications/herald/controller/HeraldRuleListController.php',
     'HeraldRulePHIDType' => 'applications/herald/phid/HeraldRulePHIDType.php',
     'HeraldRuleQuery' => 'applications/herald/query/HeraldRuleQuery.php',
     'HeraldRuleSearchEngine' => 'applications/herald/query/HeraldRuleSearchEngine.php',
     'HeraldRuleTestCase' => 'applications/herald/storage/__tests__/HeraldRuleTestCase.php',
     'HeraldRuleTransaction' => 'applications/herald/storage/HeraldRuleTransaction.php',
     'HeraldRuleTransactionComment' => 'applications/herald/storage/HeraldRuleTransactionComment.php',
     'HeraldRuleTranscript' => 'applications/herald/storage/transcript/HeraldRuleTranscript.php',
     'HeraldRuleTypeConfig' => 'applications/herald/config/HeraldRuleTypeConfig.php',
     'HeraldRuleViewController' => 'applications/herald/controller/HeraldRuleViewController.php',
     'HeraldSchemaSpec' => 'applications/herald/storage/HeraldSchemaSpec.php',
     'HeraldTestConsoleController' => 'applications/herald/controller/HeraldTestConsoleController.php',
     'HeraldTransactionQuery' => 'applications/herald/query/HeraldTransactionQuery.php',
     'HeraldTranscript' => 'applications/herald/storage/transcript/HeraldTranscript.php',
     'HeraldTranscriptController' => 'applications/herald/controller/HeraldTranscriptController.php',
     'HeraldTranscriptGarbageCollector' => 'applications/herald/garbagecollector/HeraldTranscriptGarbageCollector.php',
     'HeraldTranscriptListController' => 'applications/herald/controller/HeraldTranscriptListController.php',
     'HeraldTranscriptQuery' => 'applications/herald/query/HeraldTranscriptQuery.php',
     'HeraldTranscriptSearchEngine' => 'applications/herald/query/HeraldTranscriptSearchEngine.php',
     'HeraldTranscriptTestCase' => 'applications/herald/storage/__tests__/HeraldTranscriptTestCase.php',
     'Javelin' => 'infrastructure/javelin/Javelin.php',
     'JavelinReactorUIExample' => 'applications/uiexample/examples/JavelinReactorUIExample.php',
     'JavelinUIExample' => 'applications/uiexample/examples/JavelinUIExample.php',
     'JavelinViewExampleServerView' => 'applications/uiexample/examples/JavelinViewExampleServerView.php',
     'JavelinViewUIExample' => 'applications/uiexample/examples/JavelinViewUIExample.php',
     'LegalpadController' => 'applications/legalpad/controller/LegalpadController.php',
     'LegalpadCreateDocumentsCapability' => 'applications/legalpad/capability/LegalpadCreateDocumentsCapability.php',
     'LegalpadDAO' => 'applications/legalpad/storage/LegalpadDAO.php',
     'LegalpadDefaultEditCapability' => 'applications/legalpad/capability/LegalpadDefaultEditCapability.php',
     'LegalpadDefaultViewCapability' => 'applications/legalpad/capability/LegalpadDefaultViewCapability.php',
     'LegalpadDocument' => 'applications/legalpad/storage/LegalpadDocument.php',
     'LegalpadDocumentBody' => 'applications/legalpad/storage/LegalpadDocumentBody.php',
     'LegalpadDocumentCommentController' => 'applications/legalpad/controller/LegalpadDocumentCommentController.php',
     'LegalpadDocumentDatasource' => 'applications/legalpad/typeahead/LegalpadDocumentDatasource.php',
     'LegalpadDocumentDoneController' => 'applications/legalpad/controller/LegalpadDocumentDoneController.php',
     'LegalpadDocumentEditController' => 'applications/legalpad/controller/LegalpadDocumentEditController.php',
     'LegalpadDocumentEditor' => 'applications/legalpad/editor/LegalpadDocumentEditor.php',
     'LegalpadDocumentListController' => 'applications/legalpad/controller/LegalpadDocumentListController.php',
     'LegalpadDocumentManageController' => 'applications/legalpad/controller/LegalpadDocumentManageController.php',
     'LegalpadDocumentQuery' => 'applications/legalpad/query/LegalpadDocumentQuery.php',
     'LegalpadDocumentRemarkupRule' => 'applications/legalpad/remarkup/LegalpadDocumentRemarkupRule.php',
     'LegalpadDocumentSearchEngine' => 'applications/legalpad/query/LegalpadDocumentSearchEngine.php',
     'LegalpadDocumentSignController' => 'applications/legalpad/controller/LegalpadDocumentSignController.php',
     'LegalpadDocumentSignature' => 'applications/legalpad/storage/LegalpadDocumentSignature.php',
     'LegalpadDocumentSignatureAddController' => 'applications/legalpad/controller/LegalpadDocumentSignatureAddController.php',
     'LegalpadDocumentSignatureListController' => 'applications/legalpad/controller/LegalpadDocumentSignatureListController.php',
     'LegalpadDocumentSignatureQuery' => 'applications/legalpad/query/LegalpadDocumentSignatureQuery.php',
     'LegalpadDocumentSignatureSearchEngine' => 'applications/legalpad/query/LegalpadDocumentSignatureSearchEngine.php',
     'LegalpadDocumentSignatureVerificationController' => 'applications/legalpad/controller/LegalpadDocumentSignatureVerificationController.php',
     'LegalpadDocumentSignatureViewController' => 'applications/legalpad/controller/LegalpadDocumentSignatureViewController.php',
     'LegalpadMailReceiver' => 'applications/legalpad/mail/LegalpadMailReceiver.php',
     'LegalpadObjectNeedsSignatureEdgeType' => 'applications/legalpad/edge/LegalpadObjectNeedsSignatureEdgeType.php',
     'LegalpadReplyHandler' => 'applications/legalpad/mail/LegalpadReplyHandler.php',
     'LegalpadSchemaSpec' => 'applications/legalpad/storage/LegalpadSchemaSpec.php',
     'LegalpadSignatureNeededByObjectEdgeType' => 'applications/legalpad/edge/LegalpadSignatureNeededByObjectEdgeType.php',
     'LegalpadTransaction' => 'applications/legalpad/storage/LegalpadTransaction.php',
     'LegalpadTransactionComment' => 'applications/legalpad/storage/LegalpadTransactionComment.php',
     'LegalpadTransactionQuery' => 'applications/legalpad/query/LegalpadTransactionQuery.php',
     'LegalpadTransactionView' => 'applications/legalpad/view/LegalpadTransactionView.php',
     'LiskChunkTestCase' => 'infrastructure/storage/lisk/__tests__/LiskChunkTestCase.php',
     'LiskDAO' => 'infrastructure/storage/lisk/LiskDAO.php',
     'LiskDAOSet' => 'infrastructure/storage/lisk/LiskDAOSet.php',
     'LiskDAOTestCase' => 'infrastructure/storage/lisk/__tests__/LiskDAOTestCase.php',
     'LiskEphemeralObjectException' => 'infrastructure/storage/lisk/LiskEphemeralObjectException.php',
     'LiskFixtureTestCase' => 'infrastructure/storage/lisk/__tests__/LiskFixtureTestCase.php',
     'LiskIsolationTestCase' => 'infrastructure/storage/lisk/__tests__/LiskIsolationTestCase.php',
     'LiskIsolationTestDAO' => 'infrastructure/storage/lisk/__tests__/LiskIsolationTestDAO.php',
     'LiskIsolationTestDAOException' => 'infrastructure/storage/lisk/__tests__/LiskIsolationTestDAOException.php',
     'LiskMigrationIterator' => 'infrastructure/storage/lisk/LiskMigrationIterator.php',
     'LiskRawMigrationIterator' => 'infrastructure/storage/lisk/LiskRawMigrationIterator.php',
     'MacroConduitAPIMethod' => 'applications/macro/conduit/MacroConduitAPIMethod.php',
     'MacroCreateMemeConduitAPIMethod' => 'applications/macro/conduit/MacroCreateMemeConduitAPIMethod.php',
     'MacroQueryConduitAPIMethod' => 'applications/macro/conduit/MacroQueryConduitAPIMethod.php',
     'ManiphestAssignEmailCommand' => 'applications/maniphest/command/ManiphestAssignEmailCommand.php',
     'ManiphestAssigneeDatasource' => 'applications/maniphest/typeahead/ManiphestAssigneeDatasource.php',
     'ManiphestBatchEditController' => 'applications/maniphest/controller/ManiphestBatchEditController.php',
     'ManiphestBulkEditCapability' => 'applications/maniphest/capability/ManiphestBulkEditCapability.php',
     'ManiphestClaimEmailCommand' => 'applications/maniphest/command/ManiphestClaimEmailCommand.php',
     'ManiphestCloseEmailCommand' => 'applications/maniphest/command/ManiphestCloseEmailCommand.php',
     'ManiphestConduitAPIMethod' => 'applications/maniphest/conduit/ManiphestConduitAPIMethod.php',
     'ManiphestConfiguredCustomField' => 'applications/maniphest/field/ManiphestConfiguredCustomField.php',
     'ManiphestConstants' => 'applications/maniphest/constants/ManiphestConstants.php',
     'ManiphestController' => 'applications/maniphest/controller/ManiphestController.php',
     'ManiphestCreateMailReceiver' => 'applications/maniphest/mail/ManiphestCreateMailReceiver.php',
     'ManiphestCreateTaskConduitAPIMethod' => 'applications/maniphest/conduit/ManiphestCreateTaskConduitAPIMethod.php',
     'ManiphestCustomField' => 'applications/maniphest/field/ManiphestCustomField.php',
     'ManiphestCustomFieldNumericIndex' => 'applications/maniphest/storage/ManiphestCustomFieldNumericIndex.php',
     'ManiphestCustomFieldStatusParser' => 'applications/maniphest/field/parser/ManiphestCustomFieldStatusParser.php',
     'ManiphestCustomFieldStatusParserTestCase' => 'applications/maniphest/field/parser/__tests__/ManiphestCustomFieldStatusParserTestCase.php',
     'ManiphestCustomFieldStorage' => 'applications/maniphest/storage/ManiphestCustomFieldStorage.php',
     'ManiphestCustomFieldStringIndex' => 'applications/maniphest/storage/ManiphestCustomFieldStringIndex.php',
     'ManiphestDAO' => 'applications/maniphest/storage/ManiphestDAO.php',
     'ManiphestDefaultEditCapability' => 'applications/maniphest/capability/ManiphestDefaultEditCapability.php',
     'ManiphestDefaultViewCapability' => 'applications/maniphest/capability/ManiphestDefaultViewCapability.php',
     'ManiphestEditAssignCapability' => 'applications/maniphest/capability/ManiphestEditAssignCapability.php',
     'ManiphestEditPoliciesCapability' => 'applications/maniphest/capability/ManiphestEditPoliciesCapability.php',
     'ManiphestEditPriorityCapability' => 'applications/maniphest/capability/ManiphestEditPriorityCapability.php',
     'ManiphestEditProjectsCapability' => 'applications/maniphest/capability/ManiphestEditProjectsCapability.php',
     'ManiphestEditStatusCapability' => 'applications/maniphest/capability/ManiphestEditStatusCapability.php',
     'ManiphestEmailCommand' => 'applications/maniphest/command/ManiphestEmailCommand.php',
     'ManiphestExcelDefaultFormat' => 'applications/maniphest/export/ManiphestExcelDefaultFormat.php',
     'ManiphestExcelFormat' => 'applications/maniphest/export/ManiphestExcelFormat.php',
+    'ManiphestExcelFormatTestCase' => 'applications/maniphest/export/__tests__/ManiphestExcelFormatTestCase.php',
     'ManiphestExportController' => 'applications/maniphest/controller/ManiphestExportController.php',
     'ManiphestGetTaskTransactionsConduitAPIMethod' => 'applications/maniphest/conduit/ManiphestGetTaskTransactionsConduitAPIMethod.php',
     'ManiphestHovercardEventListener' => 'applications/maniphest/event/ManiphestHovercardEventListener.php',
     'ManiphestInfoConduitAPIMethod' => 'applications/maniphest/conduit/ManiphestInfoConduitAPIMethod.php',
     'ManiphestNameIndex' => 'applications/maniphest/storage/ManiphestNameIndex.php',
     'ManiphestNameIndexEventListener' => 'applications/maniphest/event/ManiphestNameIndexEventListener.php',
     'ManiphestPriorityEmailCommand' => 'applications/maniphest/command/ManiphestPriorityEmailCommand.php',
     'ManiphestQueryConduitAPIMethod' => 'applications/maniphest/conduit/ManiphestQueryConduitAPIMethod.php',
     'ManiphestQueryStatusesConduitAPIMethod' => 'applications/maniphest/conduit/ManiphestQueryStatusesConduitAPIMethod.php',
     'ManiphestRemarkupRule' => 'applications/maniphest/remarkup/ManiphestRemarkupRule.php',
     'ManiphestReplyHandler' => 'applications/maniphest/mail/ManiphestReplyHandler.php',
     'ManiphestReportController' => 'applications/maniphest/controller/ManiphestReportController.php',
     'ManiphestSchemaSpec' => 'applications/maniphest/storage/ManiphestSchemaSpec.php',
     'ManiphestSearchIndexer' => 'applications/maniphest/search/ManiphestSearchIndexer.php',
     'ManiphestStatusConfigOptionType' => 'applications/maniphest/config/ManiphestStatusConfigOptionType.php',
     'ManiphestStatusEmailCommand' => 'applications/maniphest/command/ManiphestStatusEmailCommand.php',
     'ManiphestSubpriorityController' => 'applications/maniphest/controller/ManiphestSubpriorityController.php',
     'ManiphestTask' => 'applications/maniphest/storage/ManiphestTask.php',
+    'ManiphestTaskAuthorPolicyRule' => 'applications/maniphest/policyrule/ManiphestTaskAuthorPolicyRule.php',
     'ManiphestTaskClosedStatusDatasource' => 'applications/maniphest/typeahead/ManiphestTaskClosedStatusDatasource.php',
     'ManiphestTaskDependedOnByTaskEdgeType' => 'applications/maniphest/edge/ManiphestTaskDependedOnByTaskEdgeType.php',
     'ManiphestTaskDependsOnTaskEdgeType' => 'applications/maniphest/edge/ManiphestTaskDependsOnTaskEdgeType.php',
     'ManiphestTaskDetailController' => 'applications/maniphest/controller/ManiphestTaskDetailController.php',
     'ManiphestTaskEditController' => 'applications/maniphest/controller/ManiphestTaskEditController.php',
     'ManiphestTaskHasCommitEdgeType' => 'applications/maniphest/edge/ManiphestTaskHasCommitEdgeType.php',
     'ManiphestTaskHasMockEdgeType' => 'applications/maniphest/edge/ManiphestTaskHasMockEdgeType.php',
     'ManiphestTaskHasRevisionEdgeType' => 'applications/maniphest/edge/ManiphestTaskHasRevisionEdgeType.php',
     'ManiphestTaskListController' => 'applications/maniphest/controller/ManiphestTaskListController.php',
     'ManiphestTaskListView' => 'applications/maniphest/view/ManiphestTaskListView.php',
     'ManiphestTaskMailReceiver' => 'applications/maniphest/mail/ManiphestTaskMailReceiver.php',
     'ManiphestTaskOpenStatusDatasource' => 'applications/maniphest/typeahead/ManiphestTaskOpenStatusDatasource.php',
     'ManiphestTaskPHIDType' => 'applications/maniphest/phid/ManiphestTaskPHIDType.php',
     'ManiphestTaskPriority' => 'applications/maniphest/constants/ManiphestTaskPriority.php',
     'ManiphestTaskPriorityDatasource' => 'applications/maniphest/typeahead/ManiphestTaskPriorityDatasource.php',
     'ManiphestTaskQuery' => 'applications/maniphest/query/ManiphestTaskQuery.php',
     'ManiphestTaskResultListView' => 'applications/maniphest/view/ManiphestTaskResultListView.php',
     'ManiphestTaskSearchEngine' => 'applications/maniphest/query/ManiphestTaskSearchEngine.php',
     'ManiphestTaskStatus' => 'applications/maniphest/constants/ManiphestTaskStatus.php',
     'ManiphestTaskStatusDatasource' => 'applications/maniphest/typeahead/ManiphestTaskStatusDatasource.php',
     'ManiphestTaskStatusFunctionDatasource' => 'applications/maniphest/typeahead/ManiphestTaskStatusFunctionDatasource.php',
     'ManiphestTaskStatusTestCase' => 'applications/maniphest/constants/__tests__/ManiphestTaskStatusTestCase.php',
     'ManiphestTaskTestCase' => 'applications/maniphest/__tests__/ManiphestTaskTestCase.php',
     'ManiphestTransaction' => 'applications/maniphest/storage/ManiphestTransaction.php',
     'ManiphestTransactionComment' => 'applications/maniphest/storage/ManiphestTransactionComment.php',
     'ManiphestTransactionEditor' => 'applications/maniphest/editor/ManiphestTransactionEditor.php',
     'ManiphestTransactionPreviewController' => 'applications/maniphest/controller/ManiphestTransactionPreviewController.php',
     'ManiphestTransactionQuery' => 'applications/maniphest/query/ManiphestTransactionQuery.php',
     'ManiphestTransactionSaveController' => 'applications/maniphest/controller/ManiphestTransactionSaveController.php',
     'ManiphestUpdateConduitAPIMethod' => 'applications/maniphest/conduit/ManiphestUpdateConduitAPIMethod.php',
     'ManiphestView' => 'applications/maniphest/view/ManiphestView.php',
     'MetaMTAConstants' => 'applications/metamta/constants/MetaMTAConstants.php',
     'MetaMTAEmailTransactionCommand' => 'applications/metamta/command/MetaMTAEmailTransactionCommand.php',
+    'MetaMTAEmailTransactionCommandTestCase' => 'applications/metamta/command/__tests__/MetaMTAEmailTransactionCommandTestCase.php',
     'MetaMTAMailReceivedGarbageCollector' => 'applications/metamta/garbagecollector/MetaMTAMailReceivedGarbageCollector.php',
     'MetaMTAMailSentGarbageCollector' => 'applications/metamta/garbagecollector/MetaMTAMailSentGarbageCollector.php',
     'MetaMTAReceivedMailStatus' => 'applications/metamta/constants/MetaMTAReceivedMailStatus.php',
     'MultimeterContext' => 'applications/multimeter/storage/MultimeterContext.php',
     'MultimeterControl' => 'applications/multimeter/data/MultimeterControl.php',
     'MultimeterController' => 'applications/multimeter/controller/MultimeterController.php',
     'MultimeterDAO' => 'applications/multimeter/storage/MultimeterDAO.php',
     'MultimeterDimension' => 'applications/multimeter/storage/MultimeterDimension.php',
     'MultimeterEvent' => 'applications/multimeter/storage/MultimeterEvent.php',
     'MultimeterEventGarbageCollector' => 'applications/multimeter/garbagecollector/MultimeterEventGarbageCollector.php',
     'MultimeterHost' => 'applications/multimeter/storage/MultimeterHost.php',
     'MultimeterLabel' => 'applications/multimeter/storage/MultimeterLabel.php',
     'MultimeterSampleController' => 'applications/multimeter/controller/MultimeterSampleController.php',
     'MultimeterViewer' => 'applications/multimeter/storage/MultimeterViewer.php',
     'NuanceConduitAPIMethod' => 'applications/nuance/conduit/NuanceConduitAPIMethod.php',
     'NuanceController' => 'applications/nuance/controller/NuanceController.php',
     'NuanceCreateItemConduitAPIMethod' => 'applications/nuance/conduit/NuanceCreateItemConduitAPIMethod.php',
     'NuanceDAO' => 'applications/nuance/storage/NuanceDAO.php',
     'NuanceItem' => 'applications/nuance/storage/NuanceItem.php',
     'NuanceItemEditController' => 'applications/nuance/controller/NuanceItemEditController.php',
     'NuanceItemEditor' => 'applications/nuance/editor/NuanceItemEditor.php',
     'NuanceItemPHIDType' => 'applications/nuance/phid/NuanceItemPHIDType.php',
     'NuanceItemQuery' => 'applications/nuance/query/NuanceItemQuery.php',
     'NuanceItemTransaction' => 'applications/nuance/storage/NuanceItemTransaction.php',
     'NuanceItemTransactionComment' => 'applications/nuance/storage/NuanceItemTransactionComment.php',
     'NuanceItemTransactionQuery' => 'applications/nuance/query/NuanceItemTransactionQuery.php',
     'NuanceItemViewController' => 'applications/nuance/controller/NuanceItemViewController.php',
     'NuancePhabricatorFormSourceDefinition' => 'applications/nuance/source/NuancePhabricatorFormSourceDefinition.php',
     'NuanceQuery' => 'applications/nuance/query/NuanceQuery.php',
     'NuanceQueue' => 'applications/nuance/storage/NuanceQueue.php',
     'NuanceQueueEditController' => 'applications/nuance/controller/NuanceQueueEditController.php',
     'NuanceQueueEditor' => 'applications/nuance/editor/NuanceQueueEditor.php',
     'NuanceQueueItem' => 'applications/nuance/storage/NuanceQueueItem.php',
     'NuanceQueueListController' => 'applications/nuance/controller/NuanceQueueListController.php',
     'NuanceQueuePHIDType' => 'applications/nuance/phid/NuanceQueuePHIDType.php',
     'NuanceQueueQuery' => 'applications/nuance/query/NuanceQueueQuery.php',
     'NuanceQueueSearchEngine' => 'applications/nuance/query/NuanceQueueSearchEngine.php',
     'NuanceQueueTransaction' => 'applications/nuance/storage/NuanceQueueTransaction.php',
     'NuanceQueueTransactionComment' => 'applications/nuance/storage/NuanceQueueTransactionComment.php',
     'NuanceQueueTransactionQuery' => 'applications/nuance/query/NuanceQueueTransactionQuery.php',
     'NuanceQueueViewController' => 'applications/nuance/controller/NuanceQueueViewController.php',
     'NuanceRequestor' => 'applications/nuance/storage/NuanceRequestor.php',
     'NuanceRequestorEditController' => 'applications/nuance/controller/NuanceRequestorEditController.php',
     'NuanceRequestorEditor' => 'applications/nuance/editor/NuanceRequestorEditor.php',
     'NuanceRequestorPHIDType' => 'applications/nuance/phid/NuanceRequestorPHIDType.php',
     'NuanceRequestorQuery' => 'applications/nuance/query/NuanceRequestorQuery.php',
     'NuanceRequestorSource' => 'applications/nuance/storage/NuanceRequestorSource.php',
     'NuanceRequestorTransaction' => 'applications/nuance/storage/NuanceRequestorTransaction.php',
     'NuanceRequestorTransactionComment' => 'applications/nuance/storage/NuanceRequestorTransactionComment.php',
     'NuanceRequestorTransactionQuery' => 'applications/nuance/query/NuanceRequestorTransactionQuery.php',
     'NuanceRequestorViewController' => 'applications/nuance/controller/NuanceRequestorViewController.php',
     'NuanceSchemaSpec' => 'applications/nuance/storage/NuanceSchemaSpec.php',
     'NuanceSource' => 'applications/nuance/storage/NuanceSource.php',
     'NuanceSourceActionController' => 'applications/nuance/controller/NuanceSourceActionController.php',
     'NuanceSourceCreateController' => 'applications/nuance/controller/NuanceSourceCreateController.php',
     'NuanceSourceDefaultEditCapability' => 'applications/nuance/capability/NuanceSourceDefaultEditCapability.php',
     'NuanceSourceDefaultViewCapability' => 'applications/nuance/capability/NuanceSourceDefaultViewCapability.php',
     'NuanceSourceDefinition' => 'applications/nuance/source/NuanceSourceDefinition.php',
+    'NuanceSourceDefinitionTestCase' => 'applications/nuance/source/__tests__/NuanceSourceDefinitionTestCase.php',
     'NuanceSourceEditController' => 'applications/nuance/controller/NuanceSourceEditController.php',
     'NuanceSourceEditor' => 'applications/nuance/editor/NuanceSourceEditor.php',
     'NuanceSourceListController' => 'applications/nuance/controller/NuanceSourceListController.php',
     'NuanceSourceManageCapability' => 'applications/nuance/capability/NuanceSourceManageCapability.php',
     'NuanceSourcePHIDType' => 'applications/nuance/phid/NuanceSourcePHIDType.php',
     'NuanceSourceQuery' => 'applications/nuance/query/NuanceSourceQuery.php',
     'NuanceSourceSearchEngine' => 'applications/nuance/query/NuanceSourceSearchEngine.php',
     'NuanceSourceTransaction' => 'applications/nuance/storage/NuanceSourceTransaction.php',
     'NuanceSourceTransactionComment' => 'applications/nuance/storage/NuanceSourceTransactionComment.php',
     'NuanceSourceTransactionQuery' => 'applications/nuance/query/NuanceSourceTransactionQuery.php',
     'NuanceSourceViewController' => 'applications/nuance/controller/NuanceSourceViewController.php',
     'NuanceTransaction' => 'applications/nuance/storage/NuanceTransaction.php',
     'OwnersConduitAPIMethod' => 'applications/owners/conduit/OwnersConduitAPIMethod.php',
     'OwnersPackageReplyHandler' => 'applications/owners/mail/OwnersPackageReplyHandler.php',
     'OwnersQueryConduitAPIMethod' => 'applications/owners/conduit/OwnersQueryConduitAPIMethod.php',
     'PHIDConduitAPIMethod' => 'applications/phid/conduit/PHIDConduitAPIMethod.php',
     'PHIDInfoConduitAPIMethod' => 'applications/phid/conduit/PHIDInfoConduitAPIMethod.php',
     'PHIDLookupConduitAPIMethod' => 'applications/phid/conduit/PHIDLookupConduitAPIMethod.php',
     'PHIDQueryConduitAPIMethod' => 'applications/phid/conduit/PHIDQueryConduitAPIMethod.php',
     'PHUI' => 'view/phui/PHUI.php',
     'PHUIActionPanelExample' => 'applications/uiexample/examples/PHUIActionPanelExample.php',
     'PHUIActionPanelView' => 'view/phui/PHUIActionPanelView.php',
     'PHUIBoxExample' => 'applications/uiexample/examples/PHUIBoxExample.php',
     'PHUIBoxView' => 'view/phui/PHUIBoxView.php',
     'PHUIButtonBarExample' => 'applications/uiexample/examples/PHUIButtonBarExample.php',
     'PHUIButtonBarView' => 'view/phui/PHUIButtonBarView.php',
     'PHUIButtonExample' => 'applications/uiexample/examples/PHUIButtonExample.php',
     'PHUIButtonView' => 'view/phui/PHUIButtonView.php',
     'PHUICalendarDayView' => 'view/phui/calendar/PHUICalendarDayView.php',
     'PHUICalendarListView' => 'view/phui/calendar/PHUICalendarListView.php',
     'PHUICalendarMonthView' => 'view/phui/calendar/PHUICalendarMonthView.php',
     'PHUICalendarWidgetView' => 'view/phui/calendar/PHUICalendarWidgetView.php',
     'PHUIColorPalletteExample' => 'applications/uiexample/examples/PHUIColorPalletteExample.php',
     'PHUICrumbView' => 'view/phui/PHUICrumbView.php',
     'PHUICrumbsView' => 'view/phui/PHUICrumbsView.php',
     'PHUIDiffInlineCommentDetailView' => 'infrastructure/diff/view/PHUIDiffInlineCommentDetailView.php',
     'PHUIDiffInlineCommentEditView' => 'infrastructure/diff/view/PHUIDiffInlineCommentEditView.php',
     'PHUIDiffInlineCommentRowScaffold' => 'infrastructure/diff/view/PHUIDiffInlineCommentRowScaffold.php',
     'PHUIDiffInlineCommentTableScaffold' => 'infrastructure/diff/view/PHUIDiffInlineCommentTableScaffold.php',
     'PHUIDiffInlineCommentUndoView' => 'infrastructure/diff/view/PHUIDiffInlineCommentUndoView.php',
     'PHUIDiffInlineCommentView' => 'infrastructure/diff/view/PHUIDiffInlineCommentView.php',
     'PHUIDiffOneUpInlineCommentRowScaffold' => 'infrastructure/diff/view/PHUIDiffOneUpInlineCommentRowScaffold.php',
     'PHUIDiffRevealIconView' => 'infrastructure/diff/view/PHUIDiffRevealIconView.php',
     'PHUIDiffTwoUpInlineCommentRowScaffold' => 'infrastructure/diff/view/PHUIDiffTwoUpInlineCommentRowScaffold.php',
     'PHUIDocumentExample' => 'applications/uiexample/examples/PHUIDocumentExample.php',
     'PHUIDocumentView' => 'view/phui/PHUIDocumentView.php',
     'PHUIFeedStoryExample' => 'applications/uiexample/examples/PHUIFeedStoryExample.php',
     'PHUIFeedStoryView' => 'view/phui/PHUIFeedStoryView.php',
     'PHUIFormDividerControl' => 'view/form/control/PHUIFormDividerControl.php',
     'PHUIFormFreeformDateControl' => 'view/form/control/PHUIFormFreeformDateControl.php',
     'PHUIFormInsetView' => 'view/form/PHUIFormInsetView.php',
     'PHUIFormLayoutView' => 'view/form/PHUIFormLayoutView.php',
     'PHUIFormMultiSubmitControl' => 'view/form/control/PHUIFormMultiSubmitControl.php',
     'PHUIFormPageView' => 'view/form/PHUIFormPageView.php',
     'PHUIHandleListView' => 'applications/phid/view/PHUIHandleListView.php',
     'PHUIHandleTagListView' => 'applications/phid/view/PHUIHandleTagListView.php',
     'PHUIHandleView' => 'applications/phid/view/PHUIHandleView.php',
     'PHUIHeaderView' => 'view/phui/PHUIHeaderView.php',
     'PHUIIconExample' => 'applications/uiexample/examples/PHUIIconExample.php',
     'PHUIIconView' => 'view/phui/PHUIIconView.php',
     'PHUIImageMaskExample' => 'applications/uiexample/examples/PHUIImageMaskExample.php',
     'PHUIImageMaskView' => 'view/phui/PHUIImageMaskView.php',
     'PHUIInfoExample' => 'applications/uiexample/examples/PHUIInfoExample.php',
     'PHUIInfoPanelExample' => 'applications/uiexample/examples/PHUIInfoPanelExample.php',
     'PHUIInfoPanelView' => 'view/phui/PHUIInfoPanelView.php',
     'PHUIInfoView' => 'view/form/PHUIInfoView.php',
     'PHUIListExample' => 'applications/uiexample/examples/PHUIListExample.php',
     'PHUIListItemView' => 'view/phui/PHUIListItemView.php',
     'PHUIListView' => 'view/phui/PHUIListView.php',
     'PHUIListViewTestCase' => 'view/layout/__tests__/PHUIListViewTestCase.php',
     'PHUIObjectBoxView' => 'view/phui/PHUIObjectBoxView.php',
     'PHUIObjectItemListExample' => 'applications/uiexample/examples/PHUIObjectItemListExample.php',
     'PHUIObjectItemListView' => 'view/phui/PHUIObjectItemListView.php',
     'PHUIObjectItemView' => 'view/phui/PHUIObjectItemView.php',
     'PHUIPagedFormView' => 'view/form/PHUIPagedFormView.php',
     'PHUIPagerView' => 'view/phui/PHUIPagerView.php',
     'PHUIPinboardItemView' => 'view/phui/PHUIPinboardItemView.php',
     'PHUIPinboardView' => 'view/phui/PHUIPinboardView.php',
     'PHUIPropertyGroupView' => 'view/phui/PHUIPropertyGroupView.php',
     'PHUIPropertyListExample' => 'applications/uiexample/examples/PHUIPropertyListExample.php',
     'PHUIPropertyListView' => 'view/phui/PHUIPropertyListView.php',
     'PHUIRemarkupPreviewPanel' => 'view/phui/PHUIRemarkupPreviewPanel.php',
     'PHUISpacesNamespaceContextView' => 'applications/spaces/view/PHUISpacesNamespaceContextView.php',
     'PHUIStatusItemView' => 'view/phui/PHUIStatusItemView.php',
     'PHUIStatusListView' => 'view/phui/PHUIStatusListView.php',
     'PHUITagExample' => 'applications/uiexample/examples/PHUITagExample.php',
     'PHUITagView' => 'view/phui/PHUITagView.php',
     'PHUITextExample' => 'applications/uiexample/examples/PHUITextExample.php',
     'PHUITextView' => 'view/phui/PHUITextView.php',
     'PHUITimelineEventView' => 'view/phui/PHUITimelineEventView.php',
     'PHUITimelineExample' => 'applications/uiexample/examples/PHUITimelineExample.php',
     'PHUITimelineView' => 'view/phui/PHUITimelineView.php',
     'PHUITypeaheadExample' => 'applications/uiexample/examples/PHUITypeaheadExample.php',
     'PHUIWorkboardView' => 'view/phui/PHUIWorkboardView.php',
     'PHUIWorkpanelView' => 'view/phui/PHUIWorkpanelView.php',
     'PassphraseAbstractKey' => 'applications/passphrase/keys/PassphraseAbstractKey.php',
     'PassphraseConduitAPIMethod' => 'applications/passphrase/conduit/PassphraseConduitAPIMethod.php',
     'PassphraseController' => 'applications/passphrase/controller/PassphraseController.php',
     'PassphraseCredential' => 'applications/passphrase/storage/PassphraseCredential.php',
     'PassphraseCredentialConduitController' => 'applications/passphrase/controller/PassphraseCredentialConduitController.php',
     'PassphraseCredentialControl' => 'applications/passphrase/view/PassphraseCredentialControl.php',
     'PassphraseCredentialCreateController' => 'applications/passphrase/controller/PassphraseCredentialCreateController.php',
     'PassphraseCredentialDestroyController' => 'applications/passphrase/controller/PassphraseCredentialDestroyController.php',
     'PassphraseCredentialEditController' => 'applications/passphrase/controller/PassphraseCredentialEditController.php',
     'PassphraseCredentialListController' => 'applications/passphrase/controller/PassphraseCredentialListController.php',
     'PassphraseCredentialLockController' => 'applications/passphrase/controller/PassphraseCredentialLockController.php',
     'PassphraseCredentialPHIDType' => 'applications/passphrase/phid/PassphraseCredentialPHIDType.php',
     'PassphraseCredentialPublicController' => 'applications/passphrase/controller/PassphraseCredentialPublicController.php',
     'PassphraseCredentialQuery' => 'applications/passphrase/query/PassphraseCredentialQuery.php',
     'PassphraseCredentialRevealController' => 'applications/passphrase/controller/PassphraseCredentialRevealController.php',
     'PassphraseCredentialSearchEngine' => 'applications/passphrase/query/PassphraseCredentialSearchEngine.php',
     'PassphraseCredentialTransaction' => 'applications/passphrase/storage/PassphraseCredentialTransaction.php',
     'PassphraseCredentialTransactionEditor' => 'applications/passphrase/editor/PassphraseCredentialTransactionEditor.php',
     'PassphraseCredentialTransactionQuery' => 'applications/passphrase/query/PassphraseCredentialTransactionQuery.php',
     'PassphraseCredentialType' => 'applications/passphrase/credentialtype/PassphraseCredentialType.php',
-    'PassphraseCredentialTypePassword' => 'applications/passphrase/credentialtype/PassphraseCredentialTypePassword.php',
-    'PassphraseCredentialTypeSSHGeneratedKey' => 'applications/passphrase/credentialtype/PassphraseCredentialTypeSSHGeneratedKey.php',
-    'PassphraseCredentialTypeSSHPrivateKey' => 'applications/passphrase/credentialtype/PassphraseCredentialTypeSSHPrivateKey.php',
-    'PassphraseCredentialTypeSSHPrivateKeyFile' => 'applications/passphrase/credentialtype/PassphraseCredentialTypeSSHPrivateKeyFile.php',
-    'PassphraseCredentialTypeSSHPrivateKeyText' => 'applications/passphrase/credentialtype/PassphraseCredentialTypeSSHPrivateKeyText.php',
+    'PassphraseCredentialTypeTestCase' => 'applications/passphrase/credentialtype/__tests__/PassphraseCredentialTypeTestCase.php',
     'PassphraseCredentialViewController' => 'applications/passphrase/controller/PassphraseCredentialViewController.php',
     'PassphraseDAO' => 'applications/passphrase/storage/PassphraseDAO.php',
+    'PassphrasePasswordCredentialType' => 'applications/passphrase/credentialtype/PassphrasePasswordCredentialType.php',
     'PassphrasePasswordKey' => 'applications/passphrase/keys/PassphrasePasswordKey.php',
     'PassphraseQueryConduitAPIMethod' => 'applications/passphrase/conduit/PassphraseQueryConduitAPIMethod.php',
     'PassphraseRemarkupRule' => 'applications/passphrase/remarkup/PassphraseRemarkupRule.php',
+    'PassphraseSSHGeneratedKeyCredentialType' => 'applications/passphrase/credentialtype/PassphraseSSHGeneratedKeyCredentialType.php',
     'PassphraseSSHKey' => 'applications/passphrase/keys/PassphraseSSHKey.php',
+    'PassphraseSSHPrivateKeyCredentialType' => 'applications/passphrase/credentialtype/PassphraseSSHPrivateKeyCredentialType.php',
+    'PassphraseSSHPrivateKeyFileCredentialType' => 'applications/passphrase/credentialtype/PassphraseSSHPrivateKeyFileCredentialType.php',
+    'PassphraseSSHPrivateKeyTextCredentialType' => 'applications/passphrase/credentialtype/PassphraseSSHPrivateKeyTextCredentialType.php',
     'PassphraseSchemaSpec' => 'applications/passphrase/storage/PassphraseSchemaSpec.php',
     'PassphraseSearchIndexer' => 'applications/passphrase/search/PassphraseSearchIndexer.php',
     'PassphraseSecret' => 'applications/passphrase/storage/PassphraseSecret.php',
     'PasteConduitAPIMethod' => 'applications/paste/conduit/PasteConduitAPIMethod.php',
     'PasteCreateConduitAPIMethod' => 'applications/paste/conduit/PasteCreateConduitAPIMethod.php',
     'PasteCreateMailReceiver' => 'applications/paste/mail/PasteCreateMailReceiver.php',
     'PasteDefaultEditCapability' => 'applications/paste/capability/PasteDefaultEditCapability.php',
     'PasteDefaultViewCapability' => 'applications/paste/capability/PasteDefaultViewCapability.php',
     'PasteEmbedView' => 'applications/paste/view/PasteEmbedView.php',
     'PasteInfoConduitAPIMethod' => 'applications/paste/conduit/PasteInfoConduitAPIMethod.php',
     'PasteMailReceiver' => 'applications/paste/mail/PasteMailReceiver.php',
     'PasteQueryConduitAPIMethod' => 'applications/paste/conduit/PasteQueryConduitAPIMethod.php',
     'PasteReplyHandler' => 'applications/paste/mail/PasteReplyHandler.php',
     'PeopleBrowseUserDirectoryCapability' => 'applications/people/capability/PeopleBrowseUserDirectoryCapability.php',
     'PeopleCreateUsersCapability' => 'applications/people/capability/PeopleCreateUsersCapability.php',
     'PeopleUserLogGarbageCollector' => 'applications/people/garbagecollector/PeopleUserLogGarbageCollector.php',
     'Phabricator404Controller' => 'applications/base/controller/Phabricator404Controller.php',
     'PhabricatorAWSConfigOptions' => 'applications/config/option/PhabricatorAWSConfigOptions.php',
     'PhabricatorAccessControlTestCase' => 'applications/base/controller/__tests__/PhabricatorAccessControlTestCase.php',
     'PhabricatorAccessLog' => 'infrastructure/log/PhabricatorAccessLog.php',
     'PhabricatorAccessLogConfigOptions' => 'applications/config/option/PhabricatorAccessLogConfigOptions.php',
     'PhabricatorAccountSettingsPanel' => 'applications/settings/panel/PhabricatorAccountSettingsPanel.php',
     'PhabricatorActionListView' => 'view/layout/PhabricatorActionListView.php',
     'PhabricatorActionView' => 'view/layout/PhabricatorActionView.php',
     'PhabricatorActivitySettingsPanel' => 'applications/settings/panel/PhabricatorActivitySettingsPanel.php',
     'PhabricatorAdministratorsPolicyRule' => 'applications/policy/rule/PhabricatorAdministratorsPolicyRule.php',
     'PhabricatorAlmanacApplication' => 'applications/almanac/application/PhabricatorAlmanacApplication.php',
     'PhabricatorAmazonAuthProvider' => 'applications/auth/provider/PhabricatorAmazonAuthProvider.php',
     'PhabricatorAnchorView' => 'view/layout/PhabricatorAnchorView.php',
     'PhabricatorAphlictManagementDebugWorkflow' => 'applications/aphlict/management/PhabricatorAphlictManagementDebugWorkflow.php',
     'PhabricatorAphlictManagementRestartWorkflow' => 'applications/aphlict/management/PhabricatorAphlictManagementRestartWorkflow.php',
     'PhabricatorAphlictManagementStartWorkflow' => 'applications/aphlict/management/PhabricatorAphlictManagementStartWorkflow.php',
     'PhabricatorAphlictManagementStatusWorkflow' => 'applications/aphlict/management/PhabricatorAphlictManagementStatusWorkflow.php',
     'PhabricatorAphlictManagementStopWorkflow' => 'applications/aphlict/management/PhabricatorAphlictManagementStopWorkflow.php',
     'PhabricatorAphlictManagementWorkflow' => 'applications/aphlict/management/PhabricatorAphlictManagementWorkflow.php',
     'PhabricatorAphlictSetupCheck' => 'applications/notification/setup/PhabricatorAphlictSetupCheck.php',
     'PhabricatorAphrontBarUIExample' => 'applications/uiexample/examples/PhabricatorAphrontBarUIExample.php',
     'PhabricatorAphrontViewTestCase' => 'view/__tests__/PhabricatorAphrontViewTestCase.php',
     'PhabricatorAppSearchEngine' => 'applications/meta/query/PhabricatorAppSearchEngine.php',
     'PhabricatorApplication' => 'applications/base/PhabricatorApplication.php',
     'PhabricatorApplicationApplicationPHIDType' => 'applications/meta/phid/PhabricatorApplicationApplicationPHIDType.php',
     'PhabricatorApplicationConfigOptions' => 'applications/config/option/PhabricatorApplicationConfigOptions.php',
     'PhabricatorApplicationConfigurationPanel' => 'applications/meta/panel/PhabricatorApplicationConfigurationPanel.php',
+    'PhabricatorApplicationConfigurationPanelTestCase' => 'applications/meta/panel/__tests__/PhabricatorApplicationConfigurationPanelTestCase.php',
     'PhabricatorApplicationDatasource' => 'applications/meta/typeahead/PhabricatorApplicationDatasource.php',
     'PhabricatorApplicationDetailViewController' => 'applications/meta/controller/PhabricatorApplicationDetailViewController.php',
     'PhabricatorApplicationEditController' => 'applications/meta/controller/PhabricatorApplicationEditController.php',
     'PhabricatorApplicationEmailCommandsController' => 'applications/meta/controller/PhabricatorApplicationEmailCommandsController.php',
     'PhabricatorApplicationLaunchView' => 'applications/meta/view/PhabricatorApplicationLaunchView.php',
     'PhabricatorApplicationPanelController' => 'applications/meta/controller/PhabricatorApplicationPanelController.php',
     'PhabricatorApplicationQuery' => 'applications/meta/query/PhabricatorApplicationQuery.php',
     'PhabricatorApplicationSearchController' => 'applications/search/controller/PhabricatorApplicationSearchController.php',
     'PhabricatorApplicationSearchEngine' => 'applications/search/engine/PhabricatorApplicationSearchEngine.php',
+    'PhabricatorApplicationSearchEngineTestCase' => 'applications/search/engine/__tests__/PhabricatorApplicationSearchEngineTestCase.php',
     'PhabricatorApplicationSearchResultsControllerInterface' => 'applications/search/interface/PhabricatorApplicationSearchResultsControllerInterface.php',
     'PhabricatorApplicationStatusView' => 'applications/meta/view/PhabricatorApplicationStatusView.php',
+    'PhabricatorApplicationTestCase' => 'applications/base/__tests__/PhabricatorApplicationTestCase.php',
     'PhabricatorApplicationTransaction' => 'applications/transactions/storage/PhabricatorApplicationTransaction.php',
     'PhabricatorApplicationTransactionComment' => 'applications/transactions/storage/PhabricatorApplicationTransactionComment.php',
     'PhabricatorApplicationTransactionCommentEditController' => 'applications/transactions/controller/PhabricatorApplicationTransactionCommentEditController.php',
     'PhabricatorApplicationTransactionCommentEditor' => 'applications/transactions/editor/PhabricatorApplicationTransactionCommentEditor.php',
     'PhabricatorApplicationTransactionCommentHistoryController' => 'applications/transactions/controller/PhabricatorApplicationTransactionCommentHistoryController.php',
     'PhabricatorApplicationTransactionCommentQuery' => 'applications/transactions/query/PhabricatorApplicationTransactionCommentQuery.php',
     'PhabricatorApplicationTransactionCommentQuoteController' => 'applications/transactions/controller/PhabricatorApplicationTransactionCommentQuoteController.php',
     'PhabricatorApplicationTransactionCommentRawController' => 'applications/transactions/controller/PhabricatorApplicationTransactionCommentRawController.php',
     'PhabricatorApplicationTransactionCommentRemoveController' => 'applications/transactions/controller/PhabricatorApplicationTransactionCommentRemoveController.php',
     'PhabricatorApplicationTransactionCommentView' => 'applications/transactions/view/PhabricatorApplicationTransactionCommentView.php',
     'PhabricatorApplicationTransactionController' => 'applications/transactions/controller/PhabricatorApplicationTransactionController.php',
     'PhabricatorApplicationTransactionDetailController' => 'applications/transactions/controller/PhabricatorApplicationTransactionDetailController.php',
     'PhabricatorApplicationTransactionEditor' => 'applications/transactions/editor/PhabricatorApplicationTransactionEditor.php',
     'PhabricatorApplicationTransactionFeedStory' => 'applications/transactions/feed/PhabricatorApplicationTransactionFeedStory.php',
     'PhabricatorApplicationTransactionInterface' => 'applications/transactions/interface/PhabricatorApplicationTransactionInterface.php',
     'PhabricatorApplicationTransactionNoEffectException' => 'applications/transactions/exception/PhabricatorApplicationTransactionNoEffectException.php',
     'PhabricatorApplicationTransactionNoEffectResponse' => 'applications/transactions/response/PhabricatorApplicationTransactionNoEffectResponse.php',
     'PhabricatorApplicationTransactionPublishWorker' => 'applications/transactions/worker/PhabricatorApplicationTransactionPublishWorker.php',
     'PhabricatorApplicationTransactionQuery' => 'applications/transactions/query/PhabricatorApplicationTransactionQuery.php',
     'PhabricatorApplicationTransactionReplyHandler' => 'applications/transactions/replyhandler/PhabricatorApplicationTransactionReplyHandler.php',
     'PhabricatorApplicationTransactionResponse' => 'applications/transactions/response/PhabricatorApplicationTransactionResponse.php',
     'PhabricatorApplicationTransactionShowOlderController' => 'applications/transactions/controller/PhabricatorApplicationTransactionShowOlderController.php',
     'PhabricatorApplicationTransactionStructureException' => 'applications/transactions/exception/PhabricatorApplicationTransactionStructureException.php',
     'PhabricatorApplicationTransactionTemplatedCommentQuery' => 'applications/transactions/query/PhabricatorApplicationTransactionTemplatedCommentQuery.php',
     'PhabricatorApplicationTransactionTextDiffDetailView' => 'applications/transactions/view/PhabricatorApplicationTransactionTextDiffDetailView.php',
     'PhabricatorApplicationTransactionTransactionPHIDType' => 'applications/transactions/phid/PhabricatorApplicationTransactionTransactionPHIDType.php',
     'PhabricatorApplicationTransactionValidationError' => 'applications/transactions/error/PhabricatorApplicationTransactionValidationError.php',
     'PhabricatorApplicationTransactionValidationException' => 'applications/transactions/exception/PhabricatorApplicationTransactionValidationException.php',
     'PhabricatorApplicationTransactionValidationResponse' => 'applications/transactions/response/PhabricatorApplicationTransactionValidationResponse.php',
     'PhabricatorApplicationTransactionValueController' => 'applications/transactions/controller/PhabricatorApplicationTransactionValueController.php',
     'PhabricatorApplicationTransactionView' => 'applications/transactions/view/PhabricatorApplicationTransactionView.php',
     'PhabricatorApplicationUninstallController' => 'applications/meta/controller/PhabricatorApplicationUninstallController.php',
     'PhabricatorApplicationsApplication' => 'applications/meta/application/PhabricatorApplicationsApplication.php',
     'PhabricatorApplicationsController' => 'applications/meta/controller/PhabricatorApplicationsController.php',
     'PhabricatorApplicationsListController' => 'applications/meta/controller/PhabricatorApplicationsListController.php',
     'PhabricatorAsanaAuthProvider' => 'applications/auth/provider/PhabricatorAsanaAuthProvider.php',
     'PhabricatorAsanaConfigOptions' => 'applications/doorkeeper/option/PhabricatorAsanaConfigOptions.php',
     'PhabricatorAsanaSubtaskHasObjectEdgeType' => 'applications/doorkeeper/edge/PhabricatorAsanaSubtaskHasObjectEdgeType.php',
     'PhabricatorAsanaTaskHasObjectEdgeType' => 'applications/doorkeeper/edge/PhabricatorAsanaTaskHasObjectEdgeType.php',
     'PhabricatorAuditActionConstants' => 'applications/audit/constants/PhabricatorAuditActionConstants.php',
     'PhabricatorAuditAddCommentController' => 'applications/audit/controller/PhabricatorAuditAddCommentController.php',
     'PhabricatorAuditApplication' => 'applications/audit/application/PhabricatorAuditApplication.php',
     'PhabricatorAuditCommentEditor' => 'applications/audit/editor/PhabricatorAuditCommentEditor.php',
     'PhabricatorAuditCommitStatusConstants' => 'applications/audit/constants/PhabricatorAuditCommitStatusConstants.php',
     'PhabricatorAuditController' => 'applications/audit/controller/PhabricatorAuditController.php',
     'PhabricatorAuditEditor' => 'applications/audit/editor/PhabricatorAuditEditor.php',
     'PhabricatorAuditInlineComment' => 'applications/audit/storage/PhabricatorAuditInlineComment.php',
     'PhabricatorAuditListController' => 'applications/audit/controller/PhabricatorAuditListController.php',
     'PhabricatorAuditListView' => 'applications/audit/view/PhabricatorAuditListView.php',
     'PhabricatorAuditMailReceiver' => 'applications/audit/mail/PhabricatorAuditMailReceiver.php',
     'PhabricatorAuditManagementDeleteWorkflow' => 'applications/audit/management/PhabricatorAuditManagementDeleteWorkflow.php',
     'PhabricatorAuditManagementWorkflow' => 'applications/audit/management/PhabricatorAuditManagementWorkflow.php',
     'PhabricatorAuditPreviewController' => 'applications/audit/controller/PhabricatorAuditPreviewController.php',
     'PhabricatorAuditReplyHandler' => 'applications/audit/mail/PhabricatorAuditReplyHandler.php',
     'PhabricatorAuditStatusConstants' => 'applications/audit/constants/PhabricatorAuditStatusConstants.php',
     'PhabricatorAuditTransaction' => 'applications/audit/storage/PhabricatorAuditTransaction.php',
     'PhabricatorAuditTransactionComment' => 'applications/audit/storage/PhabricatorAuditTransactionComment.php',
     'PhabricatorAuditTransactionQuery' => 'applications/audit/query/PhabricatorAuditTransactionQuery.php',
     'PhabricatorAuditTransactionView' => 'applications/audit/view/PhabricatorAuditTransactionView.php',
     'PhabricatorAuthAccountView' => 'applications/auth/view/PhabricatorAuthAccountView.php',
     'PhabricatorAuthApplication' => 'applications/auth/application/PhabricatorAuthApplication.php',
     'PhabricatorAuthAuthFactorPHIDType' => 'applications/auth/phid/PhabricatorAuthAuthFactorPHIDType.php',
     'PhabricatorAuthAuthProviderPHIDType' => 'applications/auth/phid/PhabricatorAuthAuthProviderPHIDType.php',
     'PhabricatorAuthConduitAPIMethod' => 'applications/auth/conduit/PhabricatorAuthConduitAPIMethod.php',
     'PhabricatorAuthConfirmLinkController' => 'applications/auth/controller/PhabricatorAuthConfirmLinkController.php',
     'PhabricatorAuthController' => 'applications/auth/controller/PhabricatorAuthController.php',
     'PhabricatorAuthDAO' => 'applications/auth/storage/PhabricatorAuthDAO.php',
     'PhabricatorAuthDisableController' => 'applications/auth/controller/config/PhabricatorAuthDisableController.php',
     'PhabricatorAuthDowngradeSessionController' => 'applications/auth/controller/PhabricatorAuthDowngradeSessionController.php',
     'PhabricatorAuthEditController' => 'applications/auth/controller/config/PhabricatorAuthEditController.php',
     'PhabricatorAuthFactor' => 'applications/auth/factor/PhabricatorAuthFactor.php',
     'PhabricatorAuthFactorConfig' => 'applications/auth/storage/PhabricatorAuthFactorConfig.php',
+    'PhabricatorAuthFactorTestCase' => 'applications/auth/factor/__tests__/PhabricatorAuthFactorTestCase.php',
     'PhabricatorAuthFinishController' => 'applications/auth/controller/PhabricatorAuthFinishController.php',
     'PhabricatorAuthHighSecurityRequiredException' => 'applications/auth/exception/PhabricatorAuthHighSecurityRequiredException.php',
     'PhabricatorAuthHighSecurityToken' => 'applications/auth/data/PhabricatorAuthHighSecurityToken.php',
     'PhabricatorAuthInvite' => 'applications/auth/storage/PhabricatorAuthInvite.php',
     'PhabricatorAuthInviteAccountException' => 'applications/auth/exception/PhabricatorAuthInviteAccountException.php',
     'PhabricatorAuthInviteAction' => 'applications/auth/data/PhabricatorAuthInviteAction.php',
     'PhabricatorAuthInviteActionTableView' => 'applications/auth/view/PhabricatorAuthInviteActionTableView.php',
     'PhabricatorAuthInviteController' => 'applications/auth/controller/PhabricatorAuthInviteController.php',
     'PhabricatorAuthInviteDialogException' => 'applications/auth/exception/PhabricatorAuthInviteDialogException.php',
     'PhabricatorAuthInviteEngine' => 'applications/auth/engine/PhabricatorAuthInviteEngine.php',
     'PhabricatorAuthInviteException' => 'applications/auth/exception/PhabricatorAuthInviteException.php',
     'PhabricatorAuthInviteInvalidException' => 'applications/auth/exception/PhabricatorAuthInviteInvalidException.php',
     'PhabricatorAuthInviteLoginException' => 'applications/auth/exception/PhabricatorAuthInviteLoginException.php',
     'PhabricatorAuthInvitePHIDType' => 'applications/auth/phid/PhabricatorAuthInvitePHIDType.php',
     'PhabricatorAuthInviteQuery' => 'applications/auth/query/PhabricatorAuthInviteQuery.php',
     'PhabricatorAuthInviteRegisteredException' => 'applications/auth/exception/PhabricatorAuthInviteRegisteredException.php',
     'PhabricatorAuthInviteSearchEngine' => 'applications/auth/query/PhabricatorAuthInviteSearchEngine.php',
     'PhabricatorAuthInviteTestCase' => 'applications/auth/factor/__tests__/PhabricatorAuthInviteTestCase.php',
     'PhabricatorAuthInviteVerifyException' => 'applications/auth/exception/PhabricatorAuthInviteVerifyException.php',
     'PhabricatorAuthInviteWorker' => 'applications/auth/worker/PhabricatorAuthInviteWorker.php',
     'PhabricatorAuthLinkController' => 'applications/auth/controller/PhabricatorAuthLinkController.php',
     'PhabricatorAuthListController' => 'applications/auth/controller/config/PhabricatorAuthListController.php',
     'PhabricatorAuthLoginController' => 'applications/auth/controller/PhabricatorAuthLoginController.php',
     'PhabricatorAuthManagementCachePKCS8Workflow' => 'applications/auth/management/PhabricatorAuthManagementCachePKCS8Workflow.php',
     'PhabricatorAuthManagementLDAPWorkflow' => 'applications/auth/management/PhabricatorAuthManagementLDAPWorkflow.php',
     'PhabricatorAuthManagementListFactorsWorkflow' => 'applications/auth/management/PhabricatorAuthManagementListFactorsWorkflow.php',
     'PhabricatorAuthManagementRecoverWorkflow' => 'applications/auth/management/PhabricatorAuthManagementRecoverWorkflow.php',
     'PhabricatorAuthManagementRefreshWorkflow' => 'applications/auth/management/PhabricatorAuthManagementRefreshWorkflow.php',
     'PhabricatorAuthManagementStripWorkflow' => 'applications/auth/management/PhabricatorAuthManagementStripWorkflow.php',
     'PhabricatorAuthManagementTrustOAuthClientWorkflow' => 'applications/auth/management/PhabricatorAuthManagementTrustOAuthClientWorkflow.php',
     'PhabricatorAuthManagementUntrustOAuthClientWorkflow' => 'applications/auth/management/PhabricatorAuthManagementUntrustOAuthClientWorkflow.php',
     'PhabricatorAuthManagementVerifyWorkflow' => 'applications/auth/management/PhabricatorAuthManagementVerifyWorkflow.php',
     'PhabricatorAuthManagementWorkflow' => 'applications/auth/management/PhabricatorAuthManagementWorkflow.php',
     'PhabricatorAuthNeedsApprovalController' => 'applications/auth/controller/PhabricatorAuthNeedsApprovalController.php',
     'PhabricatorAuthNeedsMultiFactorController' => 'applications/auth/controller/PhabricatorAuthNeedsMultiFactorController.php',
     'PhabricatorAuthNewController' => 'applications/auth/controller/config/PhabricatorAuthNewController.php',
     'PhabricatorAuthOldOAuthRedirectController' => 'applications/auth/controller/PhabricatorAuthOldOAuthRedirectController.php',
     'PhabricatorAuthOneTimeLoginController' => 'applications/auth/controller/PhabricatorAuthOneTimeLoginController.php',
     'PhabricatorAuthProvider' => 'applications/auth/provider/PhabricatorAuthProvider.php',
     'PhabricatorAuthProviderConfig' => 'applications/auth/storage/PhabricatorAuthProviderConfig.php',
     'PhabricatorAuthProviderConfigController' => 'applications/auth/controller/config/PhabricatorAuthProviderConfigController.php',
     'PhabricatorAuthProviderConfigEditor' => 'applications/auth/editor/PhabricatorAuthProviderConfigEditor.php',
     'PhabricatorAuthProviderConfigQuery' => 'applications/auth/query/PhabricatorAuthProviderConfigQuery.php',
     'PhabricatorAuthProviderConfigTransaction' => 'applications/auth/storage/PhabricatorAuthProviderConfigTransaction.php',
     'PhabricatorAuthProviderConfigTransactionQuery' => 'applications/auth/query/PhabricatorAuthProviderConfigTransactionQuery.php',
     'PhabricatorAuthQueryPublicKeysConduitAPIMethod' => 'applications/auth/conduit/PhabricatorAuthQueryPublicKeysConduitAPIMethod.php',
     'PhabricatorAuthRegisterController' => 'applications/auth/controller/PhabricatorAuthRegisterController.php',
     'PhabricatorAuthRevokeTokenController' => 'applications/auth/controller/PhabricatorAuthRevokeTokenController.php',
     'PhabricatorAuthSSHKey' => 'applications/auth/storage/PhabricatorAuthSSHKey.php',
     'PhabricatorAuthSSHKeyController' => 'applications/auth/controller/PhabricatorAuthSSHKeyController.php',
     'PhabricatorAuthSSHKeyDeleteController' => 'applications/auth/controller/PhabricatorAuthSSHKeyDeleteController.php',
     'PhabricatorAuthSSHKeyEditController' => 'applications/auth/controller/PhabricatorAuthSSHKeyEditController.php',
     'PhabricatorAuthSSHKeyGenerateController' => 'applications/auth/controller/PhabricatorAuthSSHKeyGenerateController.php',
     'PhabricatorAuthSSHKeyQuery' => 'applications/auth/query/PhabricatorAuthSSHKeyQuery.php',
     'PhabricatorAuthSSHKeyTableView' => 'applications/auth/view/PhabricatorAuthSSHKeyTableView.php',
     'PhabricatorAuthSSHPublicKey' => 'applications/auth/sshkey/PhabricatorAuthSSHPublicKey.php',
     'PhabricatorAuthSession' => 'applications/auth/storage/PhabricatorAuthSession.php',
     'PhabricatorAuthSessionEngine' => 'applications/auth/engine/PhabricatorAuthSessionEngine.php',
     'PhabricatorAuthSessionGarbageCollector' => 'applications/auth/garbagecollector/PhabricatorAuthSessionGarbageCollector.php',
     'PhabricatorAuthSessionQuery' => 'applications/auth/query/PhabricatorAuthSessionQuery.php',
     'PhabricatorAuthSetupCheck' => 'applications/config/check/PhabricatorAuthSetupCheck.php',
     'PhabricatorAuthStartController' => 'applications/auth/controller/PhabricatorAuthStartController.php',
     'PhabricatorAuthTemporaryToken' => 'applications/auth/storage/PhabricatorAuthTemporaryToken.php',
     'PhabricatorAuthTemporaryTokenGarbageCollector' => 'applications/auth/garbagecollector/PhabricatorAuthTemporaryTokenGarbageCollector.php',
     'PhabricatorAuthTemporaryTokenQuery' => 'applications/auth/query/PhabricatorAuthTemporaryTokenQuery.php',
     'PhabricatorAuthTerminateSessionController' => 'applications/auth/controller/PhabricatorAuthTerminateSessionController.php',
     'PhabricatorAuthTryFactorAction' => 'applications/auth/action/PhabricatorAuthTryFactorAction.php',
     'PhabricatorAuthUnlinkController' => 'applications/auth/controller/PhabricatorAuthUnlinkController.php',
     'PhabricatorAuthValidateController' => 'applications/auth/controller/PhabricatorAuthValidateController.php',
     'PhabricatorAuthenticationConfigOptions' => 'applications/config/option/PhabricatorAuthenticationConfigOptions.php',
     'PhabricatorAutoEventListener' => 'infrastructure/events/PhabricatorAutoEventListener.php',
     'PhabricatorBarePageUIExample' => 'applications/uiexample/examples/PhabricatorBarePageUIExample.php',
     'PhabricatorBarePageView' => 'view/page/PhabricatorBarePageView.php',
     'PhabricatorBaseURISetupCheck' => 'applications/config/check/PhabricatorBaseURISetupCheck.php',
     'PhabricatorBcryptPasswordHasher' => 'infrastructure/util/password/PhabricatorBcryptPasswordHasher.php',
     'PhabricatorBinariesSetupCheck' => 'applications/config/check/PhabricatorBinariesSetupCheck.php',
     'PhabricatorBitbucketAuthProvider' => 'applications/auth/provider/PhabricatorBitbucketAuthProvider.php',
     'PhabricatorBot' => 'infrastructure/daemon/bot/PhabricatorBot.php',
     'PhabricatorBotChannel' => 'infrastructure/daemon/bot/target/PhabricatorBotChannel.php',
     'PhabricatorBotDebugLogHandler' => 'infrastructure/daemon/bot/handler/PhabricatorBotDebugLogHandler.php',
     'PhabricatorBotFeedNotificationHandler' => 'infrastructure/daemon/bot/handler/PhabricatorBotFeedNotificationHandler.php',
     'PhabricatorBotFlowdockProtocolAdapter' => 'infrastructure/daemon/bot/adapter/PhabricatorBotFlowdockProtocolAdapter.php',
     'PhabricatorBotHandler' => 'infrastructure/daemon/bot/handler/PhabricatorBotHandler.php',
     'PhabricatorBotLogHandler' => 'infrastructure/daemon/bot/handler/PhabricatorBotLogHandler.php',
     'PhabricatorBotMacroHandler' => 'infrastructure/daemon/bot/handler/PhabricatorBotMacroHandler.php',
     'PhabricatorBotMessage' => 'infrastructure/daemon/bot/PhabricatorBotMessage.php',
     'PhabricatorBotObjectNameHandler' => 'infrastructure/daemon/bot/handler/PhabricatorBotObjectNameHandler.php',
     'PhabricatorBotSymbolHandler' => 'infrastructure/daemon/bot/handler/PhabricatorBotSymbolHandler.php',
     'PhabricatorBotTarget' => 'infrastructure/daemon/bot/target/PhabricatorBotTarget.php',
     'PhabricatorBotUser' => 'infrastructure/daemon/bot/target/PhabricatorBotUser.php',
     'PhabricatorBotWhatsNewHandler' => 'infrastructure/daemon/bot/handler/PhabricatorBotWhatsNewHandler.php',
     'PhabricatorBritishEnglishTranslation' => 'infrastructure/internationalization/translation/PhabricatorBritishEnglishTranslation.php',
     'PhabricatorBuiltinPatchList' => 'infrastructure/storage/patch/PhabricatorBuiltinPatchList.php',
     'PhabricatorBusyUIExample' => 'applications/uiexample/examples/PhabricatorBusyUIExample.php',
     'PhabricatorCacheDAO' => 'applications/cache/storage/PhabricatorCacheDAO.php',
     'PhabricatorCacheGeneralGarbageCollector' => 'applications/cache/garbagecollector/PhabricatorCacheGeneralGarbageCollector.php',
     'PhabricatorCacheManagementPurgeWorkflow' => 'applications/cache/management/PhabricatorCacheManagementPurgeWorkflow.php',
     'PhabricatorCacheManagementWorkflow' => 'applications/cache/management/PhabricatorCacheManagementWorkflow.php',
     'PhabricatorCacheMarkupGarbageCollector' => 'applications/cache/garbagecollector/PhabricatorCacheMarkupGarbageCollector.php',
     'PhabricatorCacheSchemaSpec' => 'applications/cache/storage/PhabricatorCacheSchemaSpec.php',
     'PhabricatorCacheSetupCheck' => 'applications/config/check/PhabricatorCacheSetupCheck.php',
     'PhabricatorCacheSpec' => 'applications/cache/spec/PhabricatorCacheSpec.php',
     'PhabricatorCacheTTLGarbageCollector' => 'applications/cache/garbagecollector/PhabricatorCacheTTLGarbageCollector.php',
     'PhabricatorCaches' => 'applications/cache/PhabricatorCaches.php',
     'PhabricatorCachesTestCase' => 'applications/cache/__tests__/PhabricatorCachesTestCase.php',
     'PhabricatorCalendarApplication' => 'applications/calendar/application/PhabricatorCalendarApplication.php',
     'PhabricatorCalendarController' => 'applications/calendar/controller/PhabricatorCalendarController.php',
     'PhabricatorCalendarDAO' => 'applications/calendar/storage/PhabricatorCalendarDAO.php',
     'PhabricatorCalendarEvent' => 'applications/calendar/storage/PhabricatorCalendarEvent.php',
     'PhabricatorCalendarEventCancelController' => 'applications/calendar/controller/PhabricatorCalendarEventCancelController.php',
     'PhabricatorCalendarEventCommentController' => 'applications/calendar/controller/PhabricatorCalendarEventCommentController.php',
     'PhabricatorCalendarEventDragController' => 'applications/calendar/controller/PhabricatorCalendarEventDragController.php',
     'PhabricatorCalendarEventEditController' => 'applications/calendar/controller/PhabricatorCalendarEventEditController.php',
     'PhabricatorCalendarEventEditIconController' => 'applications/calendar/controller/PhabricatorCalendarEventEditIconController.php',
     'PhabricatorCalendarEventEditor' => 'applications/calendar/editor/PhabricatorCalendarEventEditor.php',
     'PhabricatorCalendarEventEmailCommand' => 'applications/calendar/command/PhabricatorCalendarEventEmailCommand.php',
     'PhabricatorCalendarEventInvitee' => 'applications/calendar/storage/PhabricatorCalendarEventInvitee.php',
     'PhabricatorCalendarEventInviteeQuery' => 'applications/calendar/query/PhabricatorCalendarEventInviteeQuery.php',
     'PhabricatorCalendarEventJoinController' => 'applications/calendar/controller/PhabricatorCalendarEventJoinController.php',
     'PhabricatorCalendarEventListController' => 'applications/calendar/controller/PhabricatorCalendarEventListController.php',
     'PhabricatorCalendarEventMailReceiver' => 'applications/calendar/mail/PhabricatorCalendarEventMailReceiver.php',
     'PhabricatorCalendarEventPHIDType' => 'applications/calendar/phid/PhabricatorCalendarEventPHIDType.php',
     'PhabricatorCalendarEventQuery' => 'applications/calendar/query/PhabricatorCalendarEventQuery.php',
     'PhabricatorCalendarEventRSVPEmailCommand' => 'applications/calendar/command/PhabricatorCalendarEventRSVPEmailCommand.php',
     'PhabricatorCalendarEventSearchEngine' => 'applications/calendar/query/PhabricatorCalendarEventSearchEngine.php',
     'PhabricatorCalendarEventSearchIndexer' => 'applications/calendar/search/PhabricatorCalendarEventSearchIndexer.php',
     'PhabricatorCalendarEventTransaction' => 'applications/calendar/storage/PhabricatorCalendarEventTransaction.php',
     'PhabricatorCalendarEventTransactionComment' => 'applications/calendar/storage/PhabricatorCalendarEventTransactionComment.php',
     'PhabricatorCalendarEventTransactionQuery' => 'applications/calendar/query/PhabricatorCalendarEventTransactionQuery.php',
     'PhabricatorCalendarEventViewController' => 'applications/calendar/controller/PhabricatorCalendarEventViewController.php',
     'PhabricatorCalendarHoliday' => 'applications/calendar/storage/PhabricatorCalendarHoliday.php',
     'PhabricatorCalendarHolidayTestCase' => 'applications/calendar/storage/__tests__/PhabricatorCalendarHolidayTestCase.php',
     'PhabricatorCalendarIcon' => 'applications/calendar/icon/PhabricatorCalendarIcon.php',
     'PhabricatorCalendarRemarkupRule' => 'applications/calendar/remarkup/PhabricatorCalendarRemarkupRule.php',
     'PhabricatorCalendarReplyHandler' => 'applications/calendar/mail/PhabricatorCalendarReplyHandler.php',
     'PhabricatorCalendarSchemaSpec' => 'applications/calendar/storage/PhabricatorCalendarSchemaSpec.php',
     'PhabricatorCampfireProtocolAdapter' => 'infrastructure/daemon/bot/adapter/PhabricatorCampfireProtocolAdapter.php',
     'PhabricatorCelerityApplication' => 'applications/celerity/application/PhabricatorCelerityApplication.php',
     'PhabricatorCelerityTestCase' => '__tests__/PhabricatorCelerityTestCase.php',
     'PhabricatorChangeParserTestCase' => 'applications/repository/worker/__tests__/PhabricatorChangeParserTestCase.php',
     'PhabricatorChangesetResponse' => 'infrastructure/diff/PhabricatorChangesetResponse.php',
     'PhabricatorChatLogApplication' => 'applications/chatlog/application/PhabricatorChatLogApplication.php',
     'PhabricatorChatLogChannel' => 'applications/chatlog/storage/PhabricatorChatLogChannel.php',
     'PhabricatorChatLogChannelListController' => 'applications/chatlog/controller/PhabricatorChatLogChannelListController.php',
     'PhabricatorChatLogChannelLogController' => 'applications/chatlog/controller/PhabricatorChatLogChannelLogController.php',
     'PhabricatorChatLogChannelQuery' => 'applications/chatlog/query/PhabricatorChatLogChannelQuery.php',
     'PhabricatorChatLogController' => 'applications/chatlog/controller/PhabricatorChatLogController.php',
     'PhabricatorChatLogDAO' => 'applications/chatlog/storage/PhabricatorChatLogDAO.php',
     'PhabricatorChatLogEvent' => 'applications/chatlog/storage/PhabricatorChatLogEvent.php',
     'PhabricatorChatLogQuery' => 'applications/chatlog/query/PhabricatorChatLogQuery.php',
     'PhabricatorChunkedFileStorageEngine' => 'applications/files/engine/PhabricatorChunkedFileStorageEngine.php',
     'PhabricatorClusterConfigOptions' => 'applications/config/option/PhabricatorClusterConfigOptions.php',
     'PhabricatorCommitBranchesField' => 'applications/repository/customfield/PhabricatorCommitBranchesField.php',
     'PhabricatorCommitCustomField' => 'applications/repository/customfield/PhabricatorCommitCustomField.php',
     'PhabricatorCommitMergedCommitsField' => 'applications/repository/customfield/PhabricatorCommitMergedCommitsField.php',
     'PhabricatorCommitRepositoryField' => 'applications/repository/customfield/PhabricatorCommitRepositoryField.php',
     'PhabricatorCommitSearchEngine' => 'applications/audit/query/PhabricatorCommitSearchEngine.php',
     'PhabricatorCommitTagsField' => 'applications/repository/customfield/PhabricatorCommitTagsField.php',
     'PhabricatorCommonPasswords' => 'applications/auth/constants/PhabricatorCommonPasswords.php',
     'PhabricatorConduitAPIController' => 'applications/conduit/controller/PhabricatorConduitAPIController.php',
     'PhabricatorConduitApplication' => 'applications/conduit/application/PhabricatorConduitApplication.php',
     'PhabricatorConduitCertificateSettingsPanel' => 'applications/settings/panel/PhabricatorConduitCertificateSettingsPanel.php',
     'PhabricatorConduitCertificateToken' => 'applications/conduit/storage/PhabricatorConduitCertificateToken.php',
     'PhabricatorConduitConnectionLog' => 'applications/conduit/storage/PhabricatorConduitConnectionLog.php',
     'PhabricatorConduitConsoleController' => 'applications/conduit/controller/PhabricatorConduitConsoleController.php',
     'PhabricatorConduitController' => 'applications/conduit/controller/PhabricatorConduitController.php',
     'PhabricatorConduitDAO' => 'applications/conduit/storage/PhabricatorConduitDAO.php',
     'PhabricatorConduitListController' => 'applications/conduit/controller/PhabricatorConduitListController.php',
     'PhabricatorConduitLogController' => 'applications/conduit/controller/PhabricatorConduitLogController.php',
     'PhabricatorConduitLogQuery' => 'applications/conduit/query/PhabricatorConduitLogQuery.php',
     'PhabricatorConduitMethodCallLog' => 'applications/conduit/storage/PhabricatorConduitMethodCallLog.php',
     'PhabricatorConduitMethodQuery' => 'applications/conduit/query/PhabricatorConduitMethodQuery.php',
     'PhabricatorConduitSearchEngine' => 'applications/conduit/query/PhabricatorConduitSearchEngine.php',
     'PhabricatorConduitTestCase' => '__tests__/PhabricatorConduitTestCase.php',
     'PhabricatorConduitToken' => 'applications/conduit/storage/PhabricatorConduitToken.php',
     'PhabricatorConduitTokenController' => 'applications/conduit/controller/PhabricatorConduitTokenController.php',
     'PhabricatorConduitTokenEditController' => 'applications/conduit/controller/PhabricatorConduitTokenEditController.php',
     'PhabricatorConduitTokenHandshakeController' => 'applications/conduit/controller/PhabricatorConduitTokenHandshakeController.php',
     'PhabricatorConduitTokenQuery' => 'applications/conduit/query/PhabricatorConduitTokenQuery.php',
     'PhabricatorConduitTokenTerminateController' => 'applications/conduit/controller/PhabricatorConduitTokenTerminateController.php',
     'PhabricatorConduitTokensSettingsPanel' => 'applications/conduit/settings/PhabricatorConduitTokensSettingsPanel.php',
     'PhabricatorConfigAllController' => 'applications/config/controller/PhabricatorConfigAllController.php',
     'PhabricatorConfigApplication' => 'applications/config/application/PhabricatorConfigApplication.php',
     'PhabricatorConfigCacheController' => 'applications/config/controller/PhabricatorConfigCacheController.php',
     'PhabricatorConfigColumnSchema' => 'applications/config/schema/PhabricatorConfigColumnSchema.php',
     'PhabricatorConfigConfigPHIDType' => 'applications/config/phid/PhabricatorConfigConfigPHIDType.php',
     'PhabricatorConfigController' => 'applications/config/controller/PhabricatorConfigController.php',
     'PhabricatorConfigCoreSchemaSpec' => 'applications/config/schema/PhabricatorConfigCoreSchemaSpec.php',
     'PhabricatorConfigDatabaseController' => 'applications/config/controller/PhabricatorConfigDatabaseController.php',
     'PhabricatorConfigDatabaseIssueController' => 'applications/config/controller/PhabricatorConfigDatabaseIssueController.php',
     'PhabricatorConfigDatabaseSchema' => 'applications/config/schema/PhabricatorConfigDatabaseSchema.php',
     'PhabricatorConfigDatabaseSource' => 'infrastructure/env/PhabricatorConfigDatabaseSource.php',
     'PhabricatorConfigDatabaseStatusController' => 'applications/config/controller/PhabricatorConfigDatabaseStatusController.php',
     'PhabricatorConfigDefaultSource' => 'infrastructure/env/PhabricatorConfigDefaultSource.php',
     'PhabricatorConfigDictionarySource' => 'infrastructure/env/PhabricatorConfigDictionarySource.php',
     'PhabricatorConfigEditController' => 'applications/config/controller/PhabricatorConfigEditController.php',
     'PhabricatorConfigEditor' => 'applications/config/editor/PhabricatorConfigEditor.php',
     'PhabricatorConfigEntry' => 'applications/config/storage/PhabricatorConfigEntry.php',
     'PhabricatorConfigEntryDAO' => 'applications/config/storage/PhabricatorConfigEntryDAO.php',
     'PhabricatorConfigEntryQuery' => 'applications/config/query/PhabricatorConfigEntryQuery.php',
     'PhabricatorConfigFileSource' => 'infrastructure/env/PhabricatorConfigFileSource.php',
     'PhabricatorConfigGroupController' => 'applications/config/controller/PhabricatorConfigGroupController.php',
     'PhabricatorConfigHistoryController' => 'applications/config/controller/PhabricatorConfigHistoryController.php',
     'PhabricatorConfigIgnoreController' => 'applications/config/controller/PhabricatorConfigIgnoreController.php',
     'PhabricatorConfigIssueListController' => 'applications/config/controller/PhabricatorConfigIssueListController.php',
     'PhabricatorConfigIssueViewController' => 'applications/config/controller/PhabricatorConfigIssueViewController.php',
     'PhabricatorConfigJSON' => 'applications/config/json/PhabricatorConfigJSON.php',
     'PhabricatorConfigJSONOptionType' => 'applications/config/custom/PhabricatorConfigJSONOptionType.php',
     'PhabricatorConfigKeySchema' => 'applications/config/schema/PhabricatorConfigKeySchema.php',
     'PhabricatorConfigListController' => 'applications/config/controller/PhabricatorConfigListController.php',
     'PhabricatorConfigLocalSource' => 'infrastructure/env/PhabricatorConfigLocalSource.php',
     'PhabricatorConfigManagementDeleteWorkflow' => 'applications/config/management/PhabricatorConfigManagementDeleteWorkflow.php',
     'PhabricatorConfigManagementGetWorkflow' => 'applications/config/management/PhabricatorConfigManagementGetWorkflow.php',
     'PhabricatorConfigManagementListWorkflow' => 'applications/config/management/PhabricatorConfigManagementListWorkflow.php',
     'PhabricatorConfigManagementMigrateWorkflow' => 'applications/config/management/PhabricatorConfigManagementMigrateWorkflow.php',
     'PhabricatorConfigManagementSetWorkflow' => 'applications/config/management/PhabricatorConfigManagementSetWorkflow.php',
     'PhabricatorConfigManagementWorkflow' => 'applications/config/management/PhabricatorConfigManagementWorkflow.php',
     'PhabricatorConfigOption' => 'applications/config/option/PhabricatorConfigOption.php',
     'PhabricatorConfigOptionType' => 'applications/config/custom/PhabricatorConfigOptionType.php',
     'PhabricatorConfigProxySource' => 'infrastructure/env/PhabricatorConfigProxySource.php',
     'PhabricatorConfigResponse' => 'applications/config/response/PhabricatorConfigResponse.php',
     'PhabricatorConfigSchemaQuery' => 'applications/config/schema/PhabricatorConfigSchemaQuery.php',
     'PhabricatorConfigSchemaSpec' => 'applications/config/schema/PhabricatorConfigSchemaSpec.php',
     'PhabricatorConfigServerSchema' => 'applications/config/schema/PhabricatorConfigServerSchema.php',
     'PhabricatorConfigSiteSource' => 'infrastructure/env/PhabricatorConfigSiteSource.php',
     'PhabricatorConfigSource' => 'infrastructure/env/PhabricatorConfigSource.php',
     'PhabricatorConfigStackSource' => 'infrastructure/env/PhabricatorConfigStackSource.php',
     'PhabricatorConfigStorageSchema' => 'applications/config/schema/PhabricatorConfigStorageSchema.php',
     'PhabricatorConfigTableSchema' => 'applications/config/schema/PhabricatorConfigTableSchema.php',
     'PhabricatorConfigTransaction' => 'applications/config/storage/PhabricatorConfigTransaction.php',
     'PhabricatorConfigTransactionQuery' => 'applications/config/query/PhabricatorConfigTransactionQuery.php',
     'PhabricatorConfigValidationException' => 'applications/config/exception/PhabricatorConfigValidationException.php',
     'PhabricatorConfigWelcomeController' => 'applications/config/controller/PhabricatorConfigWelcomeController.php',
     'PhabricatorConpherenceApplication' => 'applications/conpherence/application/PhabricatorConpherenceApplication.php',
     'PhabricatorConpherencePreferencesSettingsPanel' => 'applications/settings/panel/PhabricatorConpherencePreferencesSettingsPanel.php',
     'PhabricatorConpherenceThreadPHIDType' => 'applications/conpherence/phid/PhabricatorConpherenceThreadPHIDType.php',
     'PhabricatorConsoleApplication' => 'applications/console/application/PhabricatorConsoleApplication.php',
     'PhabricatorContentSource' => 'applications/metamta/contentsource/PhabricatorContentSource.php',
     'PhabricatorContentSourceView' => 'applications/metamta/contentsource/PhabricatorContentSourceView.php',
     'PhabricatorContributedToObjectEdgeType' => 'applications/transactions/edges/PhabricatorContributedToObjectEdgeType.php',
     'PhabricatorController' => 'applications/base/controller/PhabricatorController.php',
     'PhabricatorCookies' => 'applications/auth/constants/PhabricatorCookies.php',
     'PhabricatorCoreConfigOptions' => 'applications/config/option/PhabricatorCoreConfigOptions.php',
     'PhabricatorCountdown' => 'applications/countdown/storage/PhabricatorCountdown.php',
     'PhabricatorCountdownApplication' => 'applications/countdown/application/PhabricatorCountdownApplication.php',
     'PhabricatorCountdownController' => 'applications/countdown/controller/PhabricatorCountdownController.php',
     'PhabricatorCountdownCountdownPHIDType' => 'applications/countdown/phid/PhabricatorCountdownCountdownPHIDType.php',
     'PhabricatorCountdownDAO' => 'applications/countdown/storage/PhabricatorCountdownDAO.php',
     'PhabricatorCountdownDefaultViewCapability' => 'applications/countdown/capability/PhabricatorCountdownDefaultViewCapability.php',
     'PhabricatorCountdownDeleteController' => 'applications/countdown/controller/PhabricatorCountdownDeleteController.php',
     'PhabricatorCountdownEditController' => 'applications/countdown/controller/PhabricatorCountdownEditController.php',
     'PhabricatorCountdownListController' => 'applications/countdown/controller/PhabricatorCountdownListController.php',
     'PhabricatorCountdownQuery' => 'applications/countdown/query/PhabricatorCountdownQuery.php',
     'PhabricatorCountdownRemarkupRule' => 'applications/countdown/remarkup/PhabricatorCountdownRemarkupRule.php',
     'PhabricatorCountdownSearchEngine' => 'applications/countdown/query/PhabricatorCountdownSearchEngine.php',
     'PhabricatorCountdownView' => 'applications/countdown/view/PhabricatorCountdownView.php',
     'PhabricatorCountdownViewController' => 'applications/countdown/controller/PhabricatorCountdownViewController.php',
     'PhabricatorCredentialsUsedByObjectEdgeType' => 'applications/passphrase/edge/PhabricatorCredentialsUsedByObjectEdgeType.php',
     'PhabricatorCursorPagedPolicyAwareQuery' => 'infrastructure/query/policy/PhabricatorCursorPagedPolicyAwareQuery.php',
     'PhabricatorCustomField' => 'infrastructure/customfield/field/PhabricatorCustomField.php',
     'PhabricatorCustomFieldAttachment' => 'infrastructure/customfield/field/PhabricatorCustomFieldAttachment.php',
     'PhabricatorCustomFieldConfigOptionType' => 'infrastructure/customfield/config/PhabricatorCustomFieldConfigOptionType.php',
     'PhabricatorCustomFieldDataNotAvailableException' => 'infrastructure/customfield/exception/PhabricatorCustomFieldDataNotAvailableException.php',
     'PhabricatorCustomFieldImplementationIncompleteException' => 'infrastructure/customfield/exception/PhabricatorCustomFieldImplementationIncompleteException.php',
     'PhabricatorCustomFieldIndexStorage' => 'infrastructure/customfield/storage/PhabricatorCustomFieldIndexStorage.php',
     'PhabricatorCustomFieldInterface' => 'infrastructure/customfield/interface/PhabricatorCustomFieldInterface.php',
     'PhabricatorCustomFieldList' => 'infrastructure/customfield/field/PhabricatorCustomFieldList.php',
     'PhabricatorCustomFieldMonogramParser' => 'infrastructure/customfield/parser/PhabricatorCustomFieldMonogramParser.php',
     'PhabricatorCustomFieldNotAttachedException' => 'infrastructure/customfield/exception/PhabricatorCustomFieldNotAttachedException.php',
     'PhabricatorCustomFieldNotProxyException' => 'infrastructure/customfield/exception/PhabricatorCustomFieldNotProxyException.php',
     'PhabricatorCustomFieldNumericIndexStorage' => 'infrastructure/customfield/storage/PhabricatorCustomFieldNumericIndexStorage.php',
     'PhabricatorCustomFieldStorage' => 'infrastructure/customfield/storage/PhabricatorCustomFieldStorage.php',
     'PhabricatorCustomFieldStringIndexStorage' => 'infrastructure/customfield/storage/PhabricatorCustomFieldStringIndexStorage.php',
     'PhabricatorCustomHeaderConfigType' => 'applications/config/custom/PhabricatorCustomHeaderConfigType.php',
     'PhabricatorDaemon' => 'infrastructure/daemon/PhabricatorDaemon.php',
     'PhabricatorDaemonConsoleController' => 'applications/daemon/controller/PhabricatorDaemonConsoleController.php',
     'PhabricatorDaemonController' => 'applications/daemon/controller/PhabricatorDaemonController.php',
     'PhabricatorDaemonDAO' => 'applications/daemon/storage/PhabricatorDaemonDAO.php',
     'PhabricatorDaemonEventListener' => 'applications/daemon/event/PhabricatorDaemonEventListener.php',
     'PhabricatorDaemonLog' => 'applications/daemon/storage/PhabricatorDaemonLog.php',
     'PhabricatorDaemonLogEvent' => 'applications/daemon/storage/PhabricatorDaemonLogEvent.php',
     'PhabricatorDaemonLogEventGarbageCollector' => 'applications/daemon/garbagecollector/PhabricatorDaemonLogEventGarbageCollector.php',
     'PhabricatorDaemonLogEventViewController' => 'applications/daemon/controller/PhabricatorDaemonLogEventViewController.php',
     'PhabricatorDaemonLogEventsView' => 'applications/daemon/view/PhabricatorDaemonLogEventsView.php',
     'PhabricatorDaemonLogGarbageCollector' => 'applications/daemon/garbagecollector/PhabricatorDaemonLogGarbageCollector.php',
     'PhabricatorDaemonLogListController' => 'applications/daemon/controller/PhabricatorDaemonLogListController.php',
     'PhabricatorDaemonLogListView' => 'applications/daemon/view/PhabricatorDaemonLogListView.php',
     'PhabricatorDaemonLogQuery' => 'applications/daemon/query/PhabricatorDaemonLogQuery.php',
     'PhabricatorDaemonLogViewController' => 'applications/daemon/controller/PhabricatorDaemonLogViewController.php',
     'PhabricatorDaemonManagementDebugWorkflow' => 'applications/daemon/management/PhabricatorDaemonManagementDebugWorkflow.php',
     'PhabricatorDaemonManagementLaunchWorkflow' => 'applications/daemon/management/PhabricatorDaemonManagementLaunchWorkflow.php',
     'PhabricatorDaemonManagementListWorkflow' => 'applications/daemon/management/PhabricatorDaemonManagementListWorkflow.php',
     'PhabricatorDaemonManagementLogWorkflow' => 'applications/daemon/management/PhabricatorDaemonManagementLogWorkflow.php',
     'PhabricatorDaemonManagementReloadWorkflow' => 'applications/daemon/management/PhabricatorDaemonManagementReloadWorkflow.php',
     'PhabricatorDaemonManagementRestartWorkflow' => 'applications/daemon/management/PhabricatorDaemonManagementRestartWorkflow.php',
     'PhabricatorDaemonManagementStartWorkflow' => 'applications/daemon/management/PhabricatorDaemonManagementStartWorkflow.php',
     'PhabricatorDaemonManagementStatusWorkflow' => 'applications/daemon/management/PhabricatorDaemonManagementStatusWorkflow.php',
     'PhabricatorDaemonManagementStopWorkflow' => 'applications/daemon/management/PhabricatorDaemonManagementStopWorkflow.php',
     'PhabricatorDaemonManagementWorkflow' => 'applications/daemon/management/PhabricatorDaemonManagementWorkflow.php',
     'PhabricatorDaemonReference' => 'infrastructure/daemon/control/PhabricatorDaemonReference.php',
     'PhabricatorDaemonTaskGarbageCollector' => 'applications/daemon/garbagecollector/PhabricatorDaemonTaskGarbageCollector.php',
     'PhabricatorDaemonTasksTableView' => 'applications/daemon/view/PhabricatorDaemonTasksTableView.php',
     'PhabricatorDaemonsApplication' => 'applications/daemon/application/PhabricatorDaemonsApplication.php',
     'PhabricatorDaemonsSetupCheck' => 'applications/config/check/PhabricatorDaemonsSetupCheck.php',
     'PhabricatorDailyRoutineTriggerClock' => 'infrastructure/daemon/workers/clock/PhabricatorDailyRoutineTriggerClock.php',
     'PhabricatorDashboard' => 'applications/dashboard/storage/PhabricatorDashboard.php',
     'PhabricatorDashboardAddPanelController' => 'applications/dashboard/controller/PhabricatorDashboardAddPanelController.php',
     'PhabricatorDashboardApplication' => 'applications/dashboard/application/PhabricatorDashboardApplication.php',
     'PhabricatorDashboardController' => 'applications/dashboard/controller/PhabricatorDashboardController.php',
     'PhabricatorDashboardCopyController' => 'applications/dashboard/controller/PhabricatorDashboardCopyController.php',
     'PhabricatorDashboardDAO' => 'applications/dashboard/storage/PhabricatorDashboardDAO.php',
     'PhabricatorDashboardDashboardHasPanelEdgeType' => 'applications/dashboard/edge/PhabricatorDashboardDashboardHasPanelEdgeType.php',
     'PhabricatorDashboardDashboardPHIDType' => 'applications/dashboard/phid/PhabricatorDashboardDashboardPHIDType.php',
     'PhabricatorDashboardEditController' => 'applications/dashboard/controller/PhabricatorDashboardEditController.php',
     'PhabricatorDashboardHistoryController' => 'applications/dashboard/controller/PhabricatorDashboardHistoryController.php',
     'PhabricatorDashboardInstall' => 'applications/dashboard/storage/PhabricatorDashboardInstall.php',
     'PhabricatorDashboardInstallController' => 'applications/dashboard/controller/PhabricatorDashboardInstallController.php',
     'PhabricatorDashboardLayoutConfig' => 'applications/dashboard/layoutconfig/PhabricatorDashboardLayoutConfig.php',
     'PhabricatorDashboardListController' => 'applications/dashboard/controller/PhabricatorDashboardListController.php',
     'PhabricatorDashboardManageController' => 'applications/dashboard/controller/PhabricatorDashboardManageController.php',
     'PhabricatorDashboardMovePanelController' => 'applications/dashboard/controller/PhabricatorDashboardMovePanelController.php',
     'PhabricatorDashboardPanel' => 'applications/dashboard/storage/PhabricatorDashboardPanel.php',
     'PhabricatorDashboardPanelArchiveController' => 'applications/dashboard/controller/PhabricatorDashboardPanelArchiveController.php',
     'PhabricatorDashboardPanelCoreCustomField' => 'applications/dashboard/customfield/PhabricatorDashboardPanelCoreCustomField.php',
     'PhabricatorDashboardPanelCustomField' => 'applications/dashboard/customfield/PhabricatorDashboardPanelCustomField.php',
     'PhabricatorDashboardPanelEditController' => 'applications/dashboard/controller/PhabricatorDashboardPanelEditController.php',
     'PhabricatorDashboardPanelHasDashboardEdgeType' => 'applications/dashboard/edge/PhabricatorDashboardPanelHasDashboardEdgeType.php',
     'PhabricatorDashboardPanelListController' => 'applications/dashboard/controller/PhabricatorDashboardPanelListController.php',
     'PhabricatorDashboardPanelPHIDType' => 'applications/dashboard/phid/PhabricatorDashboardPanelPHIDType.php',
     'PhabricatorDashboardPanelQuery' => 'applications/dashboard/query/PhabricatorDashboardPanelQuery.php',
     'PhabricatorDashboardPanelRenderController' => 'applications/dashboard/controller/PhabricatorDashboardPanelRenderController.php',
     'PhabricatorDashboardPanelRenderingEngine' => 'applications/dashboard/engine/PhabricatorDashboardPanelRenderingEngine.php',
     'PhabricatorDashboardPanelSearchApplicationCustomField' => 'applications/dashboard/customfield/PhabricatorDashboardPanelSearchApplicationCustomField.php',
     'PhabricatorDashboardPanelSearchEngine' => 'applications/dashboard/query/PhabricatorDashboardPanelSearchEngine.php',
     'PhabricatorDashboardPanelSearchQueryCustomField' => 'applications/dashboard/customfield/PhabricatorDashboardPanelSearchQueryCustomField.php',
     'PhabricatorDashboardPanelTabsCustomField' => 'applications/dashboard/customfield/PhabricatorDashboardPanelTabsCustomField.php',
     'PhabricatorDashboardPanelTransaction' => 'applications/dashboard/storage/PhabricatorDashboardPanelTransaction.php',
     'PhabricatorDashboardPanelTransactionEditor' => 'applications/dashboard/editor/PhabricatorDashboardPanelTransactionEditor.php',
     'PhabricatorDashboardPanelTransactionQuery' => 'applications/dashboard/query/PhabricatorDashboardPanelTransactionQuery.php',
     'PhabricatorDashboardPanelType' => 'applications/dashboard/paneltype/PhabricatorDashboardPanelType.php',
     'PhabricatorDashboardPanelViewController' => 'applications/dashboard/controller/PhabricatorDashboardPanelViewController.php',
     'PhabricatorDashboardQuery' => 'applications/dashboard/query/PhabricatorDashboardQuery.php',
     'PhabricatorDashboardQueryPanelType' => 'applications/dashboard/paneltype/PhabricatorDashboardQueryPanelType.php',
     'PhabricatorDashboardRemarkupRule' => 'applications/dashboard/remarkup/PhabricatorDashboardRemarkupRule.php',
     'PhabricatorDashboardRemovePanelController' => 'applications/dashboard/controller/PhabricatorDashboardRemovePanelController.php',
     'PhabricatorDashboardRenderingEngine' => 'applications/dashboard/engine/PhabricatorDashboardRenderingEngine.php',
     'PhabricatorDashboardSchemaSpec' => 'applications/dashboard/storage/PhabricatorDashboardSchemaSpec.php',
     'PhabricatorDashboardSearchEngine' => 'applications/dashboard/query/PhabricatorDashboardSearchEngine.php',
     'PhabricatorDashboardTabsPanelType' => 'applications/dashboard/paneltype/PhabricatorDashboardTabsPanelType.php',
     'PhabricatorDashboardTextPanelType' => 'applications/dashboard/paneltype/PhabricatorDashboardTextPanelType.php',
     'PhabricatorDashboardTransaction' => 'applications/dashboard/storage/PhabricatorDashboardTransaction.php',
     'PhabricatorDashboardTransactionEditor' => 'applications/dashboard/editor/PhabricatorDashboardTransactionEditor.php',
     'PhabricatorDashboardTransactionQuery' => 'applications/dashboard/query/PhabricatorDashboardTransactionQuery.php',
     'PhabricatorDashboardUninstallController' => 'applications/dashboard/controller/PhabricatorDashboardUninstallController.php',
     'PhabricatorDashboardViewController' => 'applications/dashboard/controller/PhabricatorDashboardViewController.php',
     'PhabricatorDataCacheSpec' => 'applications/cache/spec/PhabricatorDataCacheSpec.php',
     'PhabricatorDataNotAttachedException' => 'infrastructure/storage/lisk/PhabricatorDataNotAttachedException.php',
     'PhabricatorDatabaseSetupCheck' => 'applications/config/check/PhabricatorDatabaseSetupCheck.php',
     'PhabricatorDateTimeSettingsPanel' => 'applications/settings/panel/PhabricatorDateTimeSettingsPanel.php',
     'PhabricatorDebugController' => 'applications/system/controller/PhabricatorDebugController.php',
     'PhabricatorDestructibleInterface' => 'applications/system/interface/PhabricatorDestructibleInterface.php',
     'PhabricatorDestructionEngine' => 'applications/system/engine/PhabricatorDestructionEngine.php',
     'PhabricatorDeveloperConfigOptions' => 'applications/config/option/PhabricatorDeveloperConfigOptions.php',
     'PhabricatorDeveloperPreferencesSettingsPanel' => 'applications/settings/panel/PhabricatorDeveloperPreferencesSettingsPanel.php',
     'PhabricatorDiffInlineCommentQuery' => 'infrastructure/diff/query/PhabricatorDiffInlineCommentQuery.php',
     'PhabricatorDiffPreferencesSettingsPanel' => 'applications/settings/panel/PhabricatorDiffPreferencesSettingsPanel.php',
     'PhabricatorDifferenceEngine' => 'infrastructure/diff/PhabricatorDifferenceEngine.php',
     'PhabricatorDifferentialApplication' => 'applications/differential/application/PhabricatorDifferentialApplication.php',
     'PhabricatorDifferentialConfigOptions' => 'applications/differential/config/PhabricatorDifferentialConfigOptions.php',
     'PhabricatorDifferentialRevisionTestDataGenerator' => 'applications/differential/lipsum/PhabricatorDifferentialRevisionTestDataGenerator.php',
     'PhabricatorDiffusionApplication' => 'applications/diffusion/application/PhabricatorDiffusionApplication.php',
     'PhabricatorDiffusionConfigOptions' => 'applications/diffusion/config/PhabricatorDiffusionConfigOptions.php',
     'PhabricatorDisabledUserController' => 'applications/auth/controller/PhabricatorDisabledUserController.php',
     'PhabricatorDisplayPreferencesSettingsPanel' => 'applications/settings/panel/PhabricatorDisplayPreferencesSettingsPanel.php',
     'PhabricatorDisqusAuthProvider' => 'applications/auth/provider/PhabricatorDisqusAuthProvider.php',
     'PhabricatorDisqusConfigOptions' => 'applications/config/option/PhabricatorDisqusConfigOptions.php',
     'PhabricatorDivinerApplication' => 'applications/diviner/application/PhabricatorDivinerApplication.php',
     'PhabricatorDoorkeeperApplication' => 'applications/doorkeeper/application/PhabricatorDoorkeeperApplication.php',
     'PhabricatorDraft' => 'applications/draft/storage/PhabricatorDraft.php',
     'PhabricatorDraftDAO' => 'applications/draft/storage/PhabricatorDraftDAO.php',
     'PhabricatorDrydockApplication' => 'applications/drydock/application/PhabricatorDrydockApplication.php',
     'PhabricatorEdgeConfig' => 'infrastructure/edges/constants/PhabricatorEdgeConfig.php',
     'PhabricatorEdgeConstants' => 'infrastructure/edges/constants/PhabricatorEdgeConstants.php',
     'PhabricatorEdgeCycleException' => 'infrastructure/edges/exception/PhabricatorEdgeCycleException.php',
     'PhabricatorEdgeEditor' => 'infrastructure/edges/editor/PhabricatorEdgeEditor.php',
     'PhabricatorEdgeGraph' => 'infrastructure/edges/util/PhabricatorEdgeGraph.php',
     'PhabricatorEdgeQuery' => 'infrastructure/edges/query/PhabricatorEdgeQuery.php',
     'PhabricatorEdgeTestCase' => 'infrastructure/edges/__tests__/PhabricatorEdgeTestCase.php',
     'PhabricatorEdgeType' => 'infrastructure/edges/type/PhabricatorEdgeType.php',
+    'PhabricatorEdgeTypeTestCase' => 'infrastructure/edges/type/__tests__/PhabricatorEdgeTypeTestCase.php',
     'PhabricatorEditor' => 'infrastructure/PhabricatorEditor.php',
     'PhabricatorElasticSearchEngine' => 'applications/search/engine/PhabricatorElasticSearchEngine.php',
     'PhabricatorElasticSearchSetupCheck' => 'applications/config/check/PhabricatorElasticSearchSetupCheck.php',
     'PhabricatorEmailAddressesSettingsPanel' => 'applications/settings/panel/PhabricatorEmailAddressesSettingsPanel.php',
     'PhabricatorEmailFormatSettingsPanel' => 'applications/settings/panel/PhabricatorEmailFormatSettingsPanel.php',
     'PhabricatorEmailLoginController' => 'applications/auth/controller/PhabricatorEmailLoginController.php',
     'PhabricatorEmailPreferencesSettingsPanel' => 'applications/settings/panel/PhabricatorEmailPreferencesSettingsPanel.php',
     'PhabricatorEmailVerificationController' => 'applications/auth/controller/PhabricatorEmailVerificationController.php',
     'PhabricatorEmbedFileRemarkupRule' => 'applications/files/markup/PhabricatorEmbedFileRemarkupRule.php',
     'PhabricatorEmojiRemarkupRule' => 'applications/macro/markup/PhabricatorEmojiRemarkupRule.php',
     'PhabricatorEmptyQueryException' => 'infrastructure/query/PhabricatorEmptyQueryException.php',
     'PhabricatorEnv' => 'infrastructure/env/PhabricatorEnv.php',
     'PhabricatorEnvTestCase' => 'infrastructure/env/__tests__/PhabricatorEnvTestCase.php',
     'PhabricatorEvent' => 'infrastructure/events/PhabricatorEvent.php',
     'PhabricatorEventEngine' => 'infrastructure/events/PhabricatorEventEngine.php',
     'PhabricatorEventListener' => 'infrastructure/events/PhabricatorEventListener.php',
     'PhabricatorEventType' => 'infrastructure/events/constant/PhabricatorEventType.php',
     'PhabricatorExampleEventListener' => 'infrastructure/events/PhabricatorExampleEventListener.php',
     'PhabricatorExtendedPolicyInterface' => 'applications/policy/interface/PhabricatorExtendedPolicyInterface.php',
     'PhabricatorExtendingPhabricatorConfigOptions' => 'applications/config/option/PhabricatorExtendingPhabricatorConfigOptions.php',
     'PhabricatorExtensionsSetupCheck' => 'applications/config/check/PhabricatorExtensionsSetupCheck.php',
     'PhabricatorExternalAccount' => 'applications/people/storage/PhabricatorExternalAccount.php',
     'PhabricatorExternalAccountQuery' => 'applications/auth/query/PhabricatorExternalAccountQuery.php',
     'PhabricatorExternalAccountsSettingsPanel' => 'applications/settings/panel/PhabricatorExternalAccountsSettingsPanel.php',
     'PhabricatorExtraConfigSetupCheck' => 'applications/config/check/PhabricatorExtraConfigSetupCheck.php',
     'PhabricatorFacebookAuthProvider' => 'applications/auth/provider/PhabricatorFacebookAuthProvider.php',
     'PhabricatorFactAggregate' => 'applications/fact/storage/PhabricatorFactAggregate.php',
     'PhabricatorFactApplication' => 'applications/fact/application/PhabricatorFactApplication.php',
     'PhabricatorFactChartController' => 'applications/fact/controller/PhabricatorFactChartController.php',
     'PhabricatorFactController' => 'applications/fact/controller/PhabricatorFactController.php',
     'PhabricatorFactCountEngine' => 'applications/fact/engine/PhabricatorFactCountEngine.php',
     'PhabricatorFactCursor' => 'applications/fact/storage/PhabricatorFactCursor.php',
     'PhabricatorFactDAO' => 'applications/fact/storage/PhabricatorFactDAO.php',
     'PhabricatorFactDaemon' => 'applications/fact/daemon/PhabricatorFactDaemon.php',
     'PhabricatorFactEngine' => 'applications/fact/engine/PhabricatorFactEngine.php',
+    'PhabricatorFactEngineTestCase' => 'applications/fact/engine/__tests__/PhabricatorFactEngineTestCase.php',
     'PhabricatorFactHomeController' => 'applications/fact/controller/PhabricatorFactHomeController.php',
     'PhabricatorFactLastUpdatedEngine' => 'applications/fact/engine/PhabricatorFactLastUpdatedEngine.php',
     'PhabricatorFactManagementAnalyzeWorkflow' => 'applications/fact/management/PhabricatorFactManagementAnalyzeWorkflow.php',
     'PhabricatorFactManagementCursorsWorkflow' => 'applications/fact/management/PhabricatorFactManagementCursorsWorkflow.php',
     'PhabricatorFactManagementDestroyWorkflow' => 'applications/fact/management/PhabricatorFactManagementDestroyWorkflow.php',
     'PhabricatorFactManagementListWorkflow' => 'applications/fact/management/PhabricatorFactManagementListWorkflow.php',
     'PhabricatorFactManagementStatusWorkflow' => 'applications/fact/management/PhabricatorFactManagementStatusWorkflow.php',
     'PhabricatorFactManagementWorkflow' => 'applications/fact/management/PhabricatorFactManagementWorkflow.php',
     'PhabricatorFactRaw' => 'applications/fact/storage/PhabricatorFactRaw.php',
     'PhabricatorFactSimpleSpec' => 'applications/fact/spec/PhabricatorFactSimpleSpec.php',
     'PhabricatorFactSpec' => 'applications/fact/spec/PhabricatorFactSpec.php',
     'PhabricatorFactUpdateIterator' => 'applications/fact/extract/PhabricatorFactUpdateIterator.php',
     'PhabricatorFeedApplication' => 'applications/feed/application/PhabricatorFeedApplication.php',
     'PhabricatorFeedBuilder' => 'applications/feed/builder/PhabricatorFeedBuilder.php',
     'PhabricatorFeedConfigOptions' => 'applications/feed/config/PhabricatorFeedConfigOptions.php',
     'PhabricatorFeedController' => 'applications/feed/controller/PhabricatorFeedController.php',
     'PhabricatorFeedDAO' => 'applications/feed/storage/PhabricatorFeedDAO.php',
     'PhabricatorFeedDetailController' => 'applications/feed/controller/PhabricatorFeedDetailController.php',
     'PhabricatorFeedListController' => 'applications/feed/controller/PhabricatorFeedListController.php',
     'PhabricatorFeedManagementRepublishWorkflow' => 'applications/feed/management/PhabricatorFeedManagementRepublishWorkflow.php',
     'PhabricatorFeedManagementWorkflow' => 'applications/feed/management/PhabricatorFeedManagementWorkflow.php',
     'PhabricatorFeedPublicStreamController' => 'applications/feed/controller/PhabricatorFeedPublicStreamController.php',
     'PhabricatorFeedQuery' => 'applications/feed/query/PhabricatorFeedQuery.php',
     'PhabricatorFeedSearchEngine' => 'applications/feed/query/PhabricatorFeedSearchEngine.php',
     'PhabricatorFeedStory' => 'applications/feed/story/PhabricatorFeedStory.php',
     'PhabricatorFeedStoryData' => 'applications/feed/storage/PhabricatorFeedStoryData.php',
     'PhabricatorFeedStoryNotification' => 'applications/notification/storage/PhabricatorFeedStoryNotification.php',
     'PhabricatorFeedStoryPublisher' => 'applications/feed/PhabricatorFeedStoryPublisher.php',
     'PhabricatorFeedStoryReference' => 'applications/feed/storage/PhabricatorFeedStoryReference.php',
     'PhabricatorFile' => 'applications/files/storage/PhabricatorFile.php',
     'PhabricatorFileBundleLoader' => 'applications/files/query/PhabricatorFileBundleLoader.php',
     'PhabricatorFileChunk' => 'applications/files/storage/PhabricatorFileChunk.php',
     'PhabricatorFileChunkIterator' => 'applications/files/engine/PhabricatorFileChunkIterator.php',
     'PhabricatorFileChunkQuery' => 'applications/files/query/PhabricatorFileChunkQuery.php',
     'PhabricatorFileCommentController' => 'applications/files/controller/PhabricatorFileCommentController.php',
     'PhabricatorFileComposeController' => 'applications/files/controller/PhabricatorFileComposeController.php',
     'PhabricatorFileController' => 'applications/files/controller/PhabricatorFileController.php',
     'PhabricatorFileDAO' => 'applications/files/storage/PhabricatorFileDAO.php',
     'PhabricatorFileDataController' => 'applications/files/controller/PhabricatorFileDataController.php',
     'PhabricatorFileDeleteController' => 'applications/files/controller/PhabricatorFileDeleteController.php',
     'PhabricatorFileDropUploadController' => 'applications/files/controller/PhabricatorFileDropUploadController.php',
     'PhabricatorFileEditController' => 'applications/files/controller/PhabricatorFileEditController.php',
     'PhabricatorFileEditor' => 'applications/files/editor/PhabricatorFileEditor.php',
     'PhabricatorFileFilePHIDType' => 'applications/files/phid/PhabricatorFileFilePHIDType.php',
     'PhabricatorFileHasObjectEdgeType' => 'applications/files/edge/PhabricatorFileHasObjectEdgeType.php',
     'PhabricatorFileImageMacro' => 'applications/macro/storage/PhabricatorFileImageMacro.php',
     'PhabricatorFileImageTransform' => 'applications/files/transform/PhabricatorFileImageTransform.php',
     'PhabricatorFileInfoController' => 'applications/files/controller/PhabricatorFileInfoController.php',
     'PhabricatorFileLinkListView' => 'view/layout/PhabricatorFileLinkListView.php',
     'PhabricatorFileLinkView' => 'view/layout/PhabricatorFileLinkView.php',
     'PhabricatorFileListController' => 'applications/files/controller/PhabricatorFileListController.php',
     'PhabricatorFileQuery' => 'applications/files/query/PhabricatorFileQuery.php',
     'PhabricatorFileSchemaSpec' => 'applications/files/storage/PhabricatorFileSchemaSpec.php',
     'PhabricatorFileSearchEngine' => 'applications/files/query/PhabricatorFileSearchEngine.php',
     'PhabricatorFileStorageBlob' => 'applications/files/storage/PhabricatorFileStorageBlob.php',
     'PhabricatorFileStorageConfigurationException' => 'applications/files/exception/PhabricatorFileStorageConfigurationException.php',
     'PhabricatorFileStorageEngine' => 'applications/files/engine/PhabricatorFileStorageEngine.php',
+    'PhabricatorFileStorageEngineTestCase' => 'applications/files/engine/__tests__/PhabricatorFileStorageEngineTestCase.php',
     'PhabricatorFileTemporaryGarbageCollector' => 'applications/files/garbagecollector/PhabricatorFileTemporaryGarbageCollector.php',
     'PhabricatorFileTestCase' => 'applications/files/storage/__tests__/PhabricatorFileTestCase.php',
     'PhabricatorFileTestDataGenerator' => 'applications/files/lipsum/PhabricatorFileTestDataGenerator.php',
     'PhabricatorFileThumbnailTransform' => 'applications/files/transform/PhabricatorFileThumbnailTransform.php',
     'PhabricatorFileTransaction' => 'applications/files/storage/PhabricatorFileTransaction.php',
     'PhabricatorFileTransactionComment' => 'applications/files/storage/PhabricatorFileTransactionComment.php',
     'PhabricatorFileTransactionQuery' => 'applications/files/query/PhabricatorFileTransactionQuery.php',
     'PhabricatorFileTransform' => 'applications/files/transform/PhabricatorFileTransform.php',
     'PhabricatorFileTransformController' => 'applications/files/controller/PhabricatorFileTransformController.php',
     'PhabricatorFileTransformListController' => 'applications/files/controller/PhabricatorFileTransformListController.php',
+    'PhabricatorFileTransformTestCase' => 'applications/files/transform/__tests__/PhabricatorFileTransformTestCase.php',
     'PhabricatorFileUploadController' => 'applications/files/controller/PhabricatorFileUploadController.php',
     'PhabricatorFileUploadDialogController' => 'applications/files/controller/PhabricatorFileUploadDialogController.php',
     'PhabricatorFileUploadException' => 'applications/files/exception/PhabricatorFileUploadException.php',
     'PhabricatorFileinfoSetupCheck' => 'applications/config/check/PhabricatorFileinfoSetupCheck.php',
     'PhabricatorFilesApplication' => 'applications/files/application/PhabricatorFilesApplication.php',
     'PhabricatorFilesApplicationStorageEnginePanel' => 'applications/files/applicationpanel/PhabricatorFilesApplicationStorageEnginePanel.php',
     'PhabricatorFilesConfigOptions' => 'applications/files/config/PhabricatorFilesConfigOptions.php',
     'PhabricatorFilesManagementCatWorkflow' => 'applications/files/management/PhabricatorFilesManagementCatWorkflow.php',
     'PhabricatorFilesManagementCompactWorkflow' => 'applications/files/management/PhabricatorFilesManagementCompactWorkflow.php',
     'PhabricatorFilesManagementEnginesWorkflow' => 'applications/files/management/PhabricatorFilesManagementEnginesWorkflow.php',
     'PhabricatorFilesManagementMigrateWorkflow' => 'applications/files/management/PhabricatorFilesManagementMigrateWorkflow.php',
     'PhabricatorFilesManagementPurgeWorkflow' => 'applications/files/management/PhabricatorFilesManagementPurgeWorkflow.php',
     'PhabricatorFilesManagementRebuildWorkflow' => 'applications/files/management/PhabricatorFilesManagementRebuildWorkflow.php',
     'PhabricatorFilesManagementWorkflow' => 'applications/files/management/PhabricatorFilesManagementWorkflow.php',
     'PhabricatorFilesOutboundRequestAction' => 'applications/files/action/PhabricatorFilesOutboundRequestAction.php',
     'PhabricatorFlag' => 'applications/flag/storage/PhabricatorFlag.php',
     'PhabricatorFlagColor' => 'applications/flag/constants/PhabricatorFlagColor.php',
     'PhabricatorFlagConstants' => 'applications/flag/constants/PhabricatorFlagConstants.php',
     'PhabricatorFlagController' => 'applications/flag/controller/PhabricatorFlagController.php',
     'PhabricatorFlagDAO' => 'applications/flag/storage/PhabricatorFlagDAO.php',
     'PhabricatorFlagDeleteController' => 'applications/flag/controller/PhabricatorFlagDeleteController.php',
     'PhabricatorFlagEditController' => 'applications/flag/controller/PhabricatorFlagEditController.php',
     'PhabricatorFlagListController' => 'applications/flag/controller/PhabricatorFlagListController.php',
     'PhabricatorFlagQuery' => 'applications/flag/query/PhabricatorFlagQuery.php',
     'PhabricatorFlagSearchEngine' => 'applications/flag/query/PhabricatorFlagSearchEngine.php',
     'PhabricatorFlagSelectControl' => 'applications/flag/view/PhabricatorFlagSelectControl.php',
     'PhabricatorFlaggableInterface' => 'applications/flag/interface/PhabricatorFlaggableInterface.php',
     'PhabricatorFlagsApplication' => 'applications/flag/application/PhabricatorFlagsApplication.php',
     'PhabricatorFlagsUIEventListener' => 'applications/flag/events/PhabricatorFlagsUIEventListener.php',
     'PhabricatorFundApplication' => 'applications/fund/application/PhabricatorFundApplication.php',
     'PhabricatorGDSetupCheck' => 'applications/config/check/PhabricatorGDSetupCheck.php',
     'PhabricatorGarbageCollector' => 'infrastructure/daemon/garbagecollector/PhabricatorGarbageCollector.php',
     'PhabricatorGarbageCollectorConfigOptions' => 'applications/config/option/PhabricatorGarbageCollectorConfigOptions.php',
     'PhabricatorGestureUIExample' => 'applications/uiexample/examples/PhabricatorGestureUIExample.php',
     'PhabricatorGitGraphStream' => 'applications/repository/daemon/PhabricatorGitGraphStream.php',
     'PhabricatorGitHubAuthProvider' => 'applications/auth/provider/PhabricatorGitHubAuthProvider.php',
     'PhabricatorGlobalLock' => 'infrastructure/util/PhabricatorGlobalLock.php',
     'PhabricatorGlobalUploadTargetView' => 'applications/files/view/PhabricatorGlobalUploadTargetView.php',
     'PhabricatorGoogleAuthProvider' => 'applications/auth/provider/PhabricatorGoogleAuthProvider.php',
     'PhabricatorHandleList' => 'applications/phid/handle/pool/PhabricatorHandleList.php',
     'PhabricatorHandleObjectSelectorDataView' => 'applications/phid/handle/view/PhabricatorHandleObjectSelectorDataView.php',
     'PhabricatorHandlePool' => 'applications/phid/handle/pool/PhabricatorHandlePool.php',
     'PhabricatorHandlePoolTestCase' => 'applications/phid/handle/pool/__tests__/PhabricatorHandlePoolTestCase.php',
     'PhabricatorHandleQuery' => 'applications/phid/query/PhabricatorHandleQuery.php',
     'PhabricatorHarbormasterApplication' => 'applications/harbormaster/application/PhabricatorHarbormasterApplication.php',
     'PhabricatorHarbormasterConfigOptions' => 'applications/harbormaster/config/PhabricatorHarbormasterConfigOptions.php',
     'PhabricatorHash' => 'infrastructure/util/PhabricatorHash.php',
     'PhabricatorHashTestCase' => 'infrastructure/util/__tests__/PhabricatorHashTestCase.php',
     'PhabricatorHelpApplication' => 'applications/help/application/PhabricatorHelpApplication.php',
     'PhabricatorHelpController' => 'applications/help/controller/PhabricatorHelpController.php',
     'PhabricatorHelpDocumentationController' => 'applications/help/controller/PhabricatorHelpDocumentationController.php',
     'PhabricatorHelpEditorProtocolController' => 'applications/help/controller/PhabricatorHelpEditorProtocolController.php',
     'PhabricatorHelpKeyboardShortcutController' => 'applications/help/controller/PhabricatorHelpKeyboardShortcutController.php',
     'PhabricatorHeraldApplication' => 'applications/herald/application/PhabricatorHeraldApplication.php',
     'PhabricatorHomeApplication' => 'applications/home/application/PhabricatorHomeApplication.php',
     'PhabricatorHomeController' => 'applications/home/controller/PhabricatorHomeController.php',
     'PhabricatorHomeMainController' => 'applications/home/controller/PhabricatorHomeMainController.php',
     'PhabricatorHomePreferencesSettingsPanel' => 'applications/settings/panel/PhabricatorHomePreferencesSettingsPanel.php',
     'PhabricatorHomeQuickCreateController' => 'applications/home/controller/PhabricatorHomeQuickCreateController.php',
     'PhabricatorHovercardUIExample' => 'applications/uiexample/examples/PhabricatorHovercardUIExample.php',
     'PhabricatorHovercardView' => 'view/widget/hovercard/PhabricatorHovercardView.php',
     'PhabricatorHunksManagementMigrateWorkflow' => 'applications/differential/management/PhabricatorHunksManagementMigrateWorkflow.php',
     'PhabricatorHunksManagementWorkflow' => 'applications/differential/management/PhabricatorHunksManagementWorkflow.php',
     'PhabricatorIRCProtocolAdapter' => 'infrastructure/daemon/bot/adapter/PhabricatorIRCProtocolAdapter.php',
     'PhabricatorIconRemarkupRule' => 'applications/macro/markup/PhabricatorIconRemarkupRule.php',
     'PhabricatorImageMacroRemarkupRule' => 'applications/macro/markup/PhabricatorImageMacroRemarkupRule.php',
     'PhabricatorImageTransformer' => 'applications/files/PhabricatorImageTransformer.php',
     'PhabricatorImagemagickSetupCheck' => 'applications/config/check/PhabricatorImagemagickSetupCheck.php',
     'PhabricatorInfrastructureTestCase' => '__tests__/PhabricatorInfrastructureTestCase.php',
     'PhabricatorInlineCommentController' => 'infrastructure/diff/PhabricatorInlineCommentController.php',
     'PhabricatorInlineCommentInterface' => 'infrastructure/diff/interface/PhabricatorInlineCommentInterface.php',
     'PhabricatorInlineCommentPreviewController' => 'infrastructure/diff/PhabricatorInlineCommentPreviewController.php',
     'PhabricatorInlineSummaryView' => 'infrastructure/diff/view/PhabricatorInlineSummaryView.php',
     'PhabricatorInternationalizationManagementExtractWorkflow' => 'infrastructure/internationalization/management/PhabricatorInternationalizationManagementExtractWorkflow.php',
     'PhabricatorInternationalizationManagementWorkflow' => 'infrastructure/internationalization/management/PhabricatorInternationalizationManagementWorkflow.php',
     'PhabricatorInvalidConfigSetupCheck' => 'applications/config/check/PhabricatorInvalidConfigSetupCheck.php',
     'PhabricatorIteratedMD5PasswordHasher' => 'infrastructure/util/password/PhabricatorIteratedMD5PasswordHasher.php',
+    'PhabricatorIteratedMD5PasswordHasherTestCase' => 'infrastructure/util/password/__tests__/PhabricatorIteratedMD5PasswordHasherTestCase.php',
     'PhabricatorJIRAAuthProvider' => 'applications/auth/provider/PhabricatorJIRAAuthProvider.php',
     'PhabricatorJavelinLinter' => 'infrastructure/lint/linter/PhabricatorJavelinLinter.php',
     'PhabricatorJiraIssueHasObjectEdgeType' => 'applications/doorkeeper/edge/PhabricatorJiraIssueHasObjectEdgeType.php',
     'PhabricatorJumpNavHandler' => 'applications/search/engine/PhabricatorJumpNavHandler.php',
     'PhabricatorKeyValueDatabaseCache' => 'applications/cache/PhabricatorKeyValueDatabaseCache.php',
     'PhabricatorLDAPAuthProvider' => 'applications/auth/provider/PhabricatorLDAPAuthProvider.php',
     'PhabricatorLegalpadApplication' => 'applications/legalpad/application/PhabricatorLegalpadApplication.php',
     'PhabricatorLegalpadConfigOptions' => 'applications/legalpad/config/PhabricatorLegalpadConfigOptions.php',
     'PhabricatorLegalpadDocumentPHIDType' => 'applications/legalpad/phid/PhabricatorLegalpadDocumentPHIDType.php',
     'PhabricatorLegalpadSignaturePolicyRule' => 'applications/policy/rule/PhabricatorLegalpadSignaturePolicyRule.php',
     'PhabricatorLibraryTestCase' => '__tests__/PhabricatorLibraryTestCase.php',
     'PhabricatorLipsumArtist' => 'applications/lipsum/image/PhabricatorLipsumArtist.php',
     'PhabricatorLipsumGenerateWorkflow' => 'applications/lipsum/management/PhabricatorLipsumGenerateWorkflow.php',
     'PhabricatorLipsumManagementWorkflow' => 'applications/lipsum/management/PhabricatorLipsumManagementWorkflow.php',
     'PhabricatorLipsumMondrianArtist' => 'applications/lipsum/image/PhabricatorLipsumMondrianArtist.php',
     'PhabricatorLiskDAO' => 'infrastructure/storage/lisk/PhabricatorLiskDAO.php',
     'PhabricatorLiskSerializer' => 'infrastructure/storage/lisk/PhabricatorLiskSerializer.php',
     'PhabricatorListFilterUIExample' => 'applications/uiexample/examples/PhabricatorListFilterUIExample.php',
     'PhabricatorLocalDiskFileStorageEngine' => 'applications/files/engine/PhabricatorLocalDiskFileStorageEngine.php',
     'PhabricatorLocalTimeTestCase' => 'view/__tests__/PhabricatorLocalTimeTestCase.php',
     'PhabricatorLocaleScopeGuard' => 'infrastructure/internationalization/scope/PhabricatorLocaleScopeGuard.php',
     'PhabricatorLocaleScopeGuardTestCase' => 'infrastructure/internationalization/scope/__tests__/PhabricatorLocaleScopeGuardTestCase.php',
     'PhabricatorLogTriggerAction' => 'infrastructure/daemon/workers/action/PhabricatorLogTriggerAction.php',
     'PhabricatorLogoutController' => 'applications/auth/controller/PhabricatorLogoutController.php',
     'PhabricatorLunarPhasePolicyRule' => 'applications/policy/rule/PhabricatorLunarPhasePolicyRule.php',
     'PhabricatorMacroApplication' => 'applications/macro/application/PhabricatorMacroApplication.php',
     'PhabricatorMacroAudioController' => 'applications/macro/controller/PhabricatorMacroAudioController.php',
     'PhabricatorMacroCommentController' => 'applications/macro/controller/PhabricatorMacroCommentController.php',
     'PhabricatorMacroConfigOptions' => 'applications/macro/config/PhabricatorMacroConfigOptions.php',
     'PhabricatorMacroController' => 'applications/macro/controller/PhabricatorMacroController.php',
     'PhabricatorMacroDatasource' => 'applications/macro/typeahead/PhabricatorMacroDatasource.php',
     'PhabricatorMacroDisableController' => 'applications/macro/controller/PhabricatorMacroDisableController.php',
     'PhabricatorMacroEditController' => 'applications/macro/controller/PhabricatorMacroEditController.php',
     'PhabricatorMacroEditor' => 'applications/macro/editor/PhabricatorMacroEditor.php',
     'PhabricatorMacroListController' => 'applications/macro/controller/PhabricatorMacroListController.php',
     'PhabricatorMacroMacroPHIDType' => 'applications/macro/phid/PhabricatorMacroMacroPHIDType.php',
     'PhabricatorMacroMailReceiver' => 'applications/macro/mail/PhabricatorMacroMailReceiver.php',
     'PhabricatorMacroManageCapability' => 'applications/macro/capability/PhabricatorMacroManageCapability.php',
     'PhabricatorMacroMemeController' => 'applications/macro/controller/PhabricatorMacroMemeController.php',
     'PhabricatorMacroMemeDialogController' => 'applications/macro/controller/PhabricatorMacroMemeDialogController.php',
     'PhabricatorMacroQuery' => 'applications/macro/query/PhabricatorMacroQuery.php',
     'PhabricatorMacroReplyHandler' => 'applications/macro/mail/PhabricatorMacroReplyHandler.php',
     'PhabricatorMacroSearchEngine' => 'applications/macro/query/PhabricatorMacroSearchEngine.php',
     'PhabricatorMacroTransaction' => 'applications/macro/storage/PhabricatorMacroTransaction.php',
     'PhabricatorMacroTransactionComment' => 'applications/macro/storage/PhabricatorMacroTransactionComment.php',
     'PhabricatorMacroTransactionQuery' => 'applications/macro/query/PhabricatorMacroTransactionQuery.php',
     'PhabricatorMacroViewController' => 'applications/macro/controller/PhabricatorMacroViewController.php',
     'PhabricatorMailImplementationAdapter' => 'applications/metamta/adapter/PhabricatorMailImplementationAdapter.php',
     'PhabricatorMailImplementationAmazonSESAdapter' => 'applications/metamta/adapter/PhabricatorMailImplementationAmazonSESAdapter.php',
     'PhabricatorMailImplementationMailgunAdapter' => 'applications/metamta/adapter/PhabricatorMailImplementationMailgunAdapter.php',
     'PhabricatorMailImplementationPHPMailerAdapter' => 'applications/metamta/adapter/PhabricatorMailImplementationPHPMailerAdapter.php',
     'PhabricatorMailImplementationPHPMailerLiteAdapter' => 'applications/metamta/adapter/PhabricatorMailImplementationPHPMailerLiteAdapter.php',
     'PhabricatorMailImplementationSendGridAdapter' => 'applications/metamta/adapter/PhabricatorMailImplementationSendGridAdapter.php',
     'PhabricatorMailImplementationTestAdapter' => 'applications/metamta/adapter/PhabricatorMailImplementationTestAdapter.php',
     'PhabricatorMailManagementListInboundWorkflow' => 'applications/metamta/management/PhabricatorMailManagementListInboundWorkflow.php',
     'PhabricatorMailManagementListOutboundWorkflow' => 'applications/metamta/management/PhabricatorMailManagementListOutboundWorkflow.php',
     'PhabricatorMailManagementReceiveTestWorkflow' => 'applications/metamta/management/PhabricatorMailManagementReceiveTestWorkflow.php',
     'PhabricatorMailManagementResendWorkflow' => 'applications/metamta/management/PhabricatorMailManagementResendWorkflow.php',
     'PhabricatorMailManagementSendTestWorkflow' => 'applications/metamta/management/PhabricatorMailManagementSendTestWorkflow.php',
     'PhabricatorMailManagementShowInboundWorkflow' => 'applications/metamta/management/PhabricatorMailManagementShowInboundWorkflow.php',
     'PhabricatorMailManagementShowOutboundWorkflow' => 'applications/metamta/management/PhabricatorMailManagementShowOutboundWorkflow.php',
     'PhabricatorMailManagementWorkflow' => 'applications/metamta/management/PhabricatorMailManagementWorkflow.php',
     'PhabricatorMailReceiver' => 'applications/metamta/receiver/PhabricatorMailReceiver.php',
     'PhabricatorMailReceiverTestCase' => 'applications/metamta/receiver/__tests__/PhabricatorMailReceiverTestCase.php',
     'PhabricatorMailReplyHandler' => 'applications/metamta/replyhandler/PhabricatorMailReplyHandler.php',
     'PhabricatorMailSetupCheck' => 'applications/config/check/PhabricatorMailSetupCheck.php',
     'PhabricatorMailTarget' => 'applications/metamta/replyhandler/PhabricatorMailTarget.php',
     'PhabricatorMailgunConfigOptions' => 'applications/config/option/PhabricatorMailgunConfigOptions.php',
     'PhabricatorMainMenuSearchView' => 'view/page/menu/PhabricatorMainMenuSearchView.php',
     'PhabricatorMainMenuView' => 'view/page/menu/PhabricatorMainMenuView.php',
     'PhabricatorManagementWorkflow' => 'infrastructure/management/PhabricatorManagementWorkflow.php',
     'PhabricatorManiphestApplication' => 'applications/maniphest/application/PhabricatorManiphestApplication.php',
     'PhabricatorManiphestConfigOptions' => 'applications/maniphest/config/PhabricatorManiphestConfigOptions.php',
     'PhabricatorManiphestTaskTestDataGenerator' => 'applications/maniphest/lipsum/PhabricatorManiphestTaskTestDataGenerator.php',
     'PhabricatorMarkupCache' => 'applications/cache/storage/PhabricatorMarkupCache.php',
     'PhabricatorMarkupEngine' => 'infrastructure/markup/PhabricatorMarkupEngine.php',
     'PhabricatorMarkupInterface' => 'infrastructure/markup/PhabricatorMarkupInterface.php',
     'PhabricatorMarkupOneOff' => 'infrastructure/markup/PhabricatorMarkupOneOff.php',
     'PhabricatorMarkupPreviewController' => 'infrastructure/markup/PhabricatorMarkupPreviewController.php',
     'PhabricatorMemeRemarkupRule' => 'applications/macro/markup/PhabricatorMemeRemarkupRule.php',
     'PhabricatorMentionRemarkupRule' => 'applications/people/markup/PhabricatorMentionRemarkupRule.php',
     'PhabricatorMentionableInterface' => 'applications/transactions/interface/PhabricatorMentionableInterface.php',
     'PhabricatorMercurialGraphStream' => 'applications/repository/daemon/PhabricatorMercurialGraphStream.php',
     'PhabricatorMetaMTAActor' => 'applications/metamta/query/PhabricatorMetaMTAActor.php',
     'PhabricatorMetaMTAActorQuery' => 'applications/metamta/query/PhabricatorMetaMTAActorQuery.php',
     'PhabricatorMetaMTAApplication' => 'applications/metamta/application/PhabricatorMetaMTAApplication.php',
     'PhabricatorMetaMTAApplicationEmail' => 'applications/metamta/storage/PhabricatorMetaMTAApplicationEmail.php',
     'PhabricatorMetaMTAApplicationEmailDatasource' => 'applications/metamta/typeahead/PhabricatorMetaMTAApplicationEmailDatasource.php',
+    'PhabricatorMetaMTAApplicationEmailEditor' => 'applications/metamta/editor/PhabricatorMetaMTAApplicationEmailEditor.php',
     'PhabricatorMetaMTAApplicationEmailPHIDType' => 'applications/phid/PhabricatorMetaMTAApplicationEmailPHIDType.php',
     'PhabricatorMetaMTAApplicationEmailPanel' => 'applications/metamta/applicationpanel/PhabricatorMetaMTAApplicationEmailPanel.php',
     'PhabricatorMetaMTAApplicationEmailQuery' => 'applications/metamta/query/PhabricatorMetaMTAApplicationEmailQuery.php',
+    'PhabricatorMetaMTAApplicationEmailTransaction' => 'applications/metamta/storage/PhabricatorMetaMTAApplicationEmailTransaction.php',
+    'PhabricatorMetaMTAApplicationEmailTransactionQuery' => 'applications/metamta/query/PhabricatorMetaMTAApplicationEmailTransactionQuery.php',
     'PhabricatorMetaMTAAttachment' => 'applications/metamta/storage/PhabricatorMetaMTAAttachment.php',
     'PhabricatorMetaMTAConfigOptions' => 'applications/config/option/PhabricatorMetaMTAConfigOptions.php',
     'PhabricatorMetaMTAController' => 'applications/metamta/controller/PhabricatorMetaMTAController.php',
     'PhabricatorMetaMTADAO' => 'applications/metamta/storage/PhabricatorMetaMTADAO.php',
     'PhabricatorMetaMTAEmailBodyParser' => 'applications/metamta/parser/PhabricatorMetaMTAEmailBodyParser.php',
     'PhabricatorMetaMTAEmailBodyParserTestCase' => 'applications/metamta/parser/__tests__/PhabricatorMetaMTAEmailBodyParserTestCase.php',
     'PhabricatorMetaMTAErrorMailAction' => 'applications/metamta/action/PhabricatorMetaMTAErrorMailAction.php',
     'PhabricatorMetaMTAMail' => 'applications/metamta/storage/PhabricatorMetaMTAMail.php',
     'PhabricatorMetaMTAMailBody' => 'applications/metamta/view/PhabricatorMetaMTAMailBody.php',
     'PhabricatorMetaMTAMailBodyTestCase' => 'applications/metamta/view/__tests__/PhabricatorMetaMTAMailBodyTestCase.php',
     'PhabricatorMetaMTAMailSection' => 'applications/metamta/view/PhabricatorMetaMTAMailSection.php',
     'PhabricatorMetaMTAMailTestCase' => 'applications/metamta/storage/__tests__/PhabricatorMetaMTAMailTestCase.php',
     'PhabricatorMetaMTAMailableDatasource' => 'applications/metamta/typeahead/PhabricatorMetaMTAMailableDatasource.php',
     'PhabricatorMetaMTAMailableFunctionDatasource' => 'applications/metamta/typeahead/PhabricatorMetaMTAMailableFunctionDatasource.php',
     'PhabricatorMetaMTAMailgunReceiveController' => 'applications/metamta/controller/PhabricatorMetaMTAMailgunReceiveController.php',
     'PhabricatorMetaMTAMailingList' => 'applications/mailinglists/storage/PhabricatorMetaMTAMailingList.php',
     'PhabricatorMetaMTAMemberQuery' => 'applications/metamta/query/PhabricatorMetaMTAMemberQuery.php',
     'PhabricatorMetaMTAPermanentFailureException' => 'applications/metamta/exception/PhabricatorMetaMTAPermanentFailureException.php',
     'PhabricatorMetaMTAReceivedMail' => 'applications/metamta/storage/PhabricatorMetaMTAReceivedMail.php',
     'PhabricatorMetaMTAReceivedMailProcessingException' => 'applications/metamta/exception/PhabricatorMetaMTAReceivedMailProcessingException.php',
     'PhabricatorMetaMTAReceivedMailTestCase' => 'applications/metamta/storage/__tests__/PhabricatorMetaMTAReceivedMailTestCase.php',
     'PhabricatorMetaMTASchemaSpec' => 'applications/metamta/storage/PhabricatorMetaMTASchemaSpec.php',
     'PhabricatorMetaMTASendGridReceiveController' => 'applications/metamta/controller/PhabricatorMetaMTASendGridReceiveController.php',
     'PhabricatorMetaMTAWorker' => 'applications/metamta/PhabricatorMetaMTAWorker.php',
     'PhabricatorMetronomicTriggerClock' => 'infrastructure/daemon/workers/clock/PhabricatorMetronomicTriggerClock.php',
     'PhabricatorMultiColumnUIExample' => 'applications/uiexample/examples/PhabricatorMultiColumnUIExample.php',
     'PhabricatorMultiFactorSettingsPanel' => 'applications/settings/panel/PhabricatorMultiFactorSettingsPanel.php',
     'PhabricatorMultimeterApplication' => 'applications/multimeter/application/PhabricatorMultimeterApplication.php',
     'PhabricatorMustVerifyEmailController' => 'applications/auth/controller/PhabricatorMustVerifyEmailController.php',
     'PhabricatorMySQLConfigOptions' => 'applications/config/option/PhabricatorMySQLConfigOptions.php',
     'PhabricatorMySQLFileStorageEngine' => 'applications/files/engine/PhabricatorMySQLFileStorageEngine.php',
     'PhabricatorMySQLSearchEngine' => 'applications/search/engine/PhabricatorMySQLSearchEngine.php',
     'PhabricatorMySQLSetupCheck' => 'applications/config/check/PhabricatorMySQLSetupCheck.php',
     'PhabricatorNamedQuery' => 'applications/search/storage/PhabricatorNamedQuery.php',
     'PhabricatorNamedQueryQuery' => 'applications/search/query/PhabricatorNamedQueryQuery.php',
     'PhabricatorNavigationRemarkupRule' => 'infrastructure/markup/rule/PhabricatorNavigationRemarkupRule.php',
     'PhabricatorNeverTriggerClock' => 'infrastructure/daemon/workers/clock/PhabricatorNeverTriggerClock.php',
     'PhabricatorNotificationAdHocFeedStory' => 'applications/notification/feed/PhabricatorNotificationAdHocFeedStory.php',
     'PhabricatorNotificationBuilder' => 'applications/notification/builder/PhabricatorNotificationBuilder.php',
     'PhabricatorNotificationClearController' => 'applications/notification/controller/PhabricatorNotificationClearController.php',
     'PhabricatorNotificationClient' => 'applications/notification/client/PhabricatorNotificationClient.php',
     'PhabricatorNotificationConfigOptions' => 'applications/config/option/PhabricatorNotificationConfigOptions.php',
     'PhabricatorNotificationController' => 'applications/notification/controller/PhabricatorNotificationController.php',
     'PhabricatorNotificationIndividualController' => 'applications/notification/controller/PhabricatorNotificationIndividualController.php',
     'PhabricatorNotificationListController' => 'applications/notification/controller/PhabricatorNotificationListController.php',
     'PhabricatorNotificationPanelController' => 'applications/notification/controller/PhabricatorNotificationPanelController.php',
     'PhabricatorNotificationQuery' => 'applications/notification/query/PhabricatorNotificationQuery.php',
     'PhabricatorNotificationSearchEngine' => 'applications/notification/query/PhabricatorNotificationSearchEngine.php',
     'PhabricatorNotificationStatusController' => 'applications/notification/controller/PhabricatorNotificationStatusController.php',
     'PhabricatorNotificationStatusView' => 'applications/notification/view/PhabricatorNotificationStatusView.php',
     'PhabricatorNotificationTestController' => 'applications/notification/controller/PhabricatorNotificationTestController.php',
     'PhabricatorNotificationUIExample' => 'applications/uiexample/examples/PhabricatorNotificationUIExample.php',
     'PhabricatorNotificationsApplication' => 'applications/notification/application/PhabricatorNotificationsApplication.php',
     'PhabricatorNuanceApplication' => 'applications/nuance/application/PhabricatorNuanceApplication.php',
     'PhabricatorOAuth1AuthProvider' => 'applications/auth/provider/PhabricatorOAuth1AuthProvider.php',
     'PhabricatorOAuth2AuthProvider' => 'applications/auth/provider/PhabricatorOAuth2AuthProvider.php',
     'PhabricatorOAuthAuthProvider' => 'applications/auth/provider/PhabricatorOAuthAuthProvider.php',
     'PhabricatorOAuthClientAuthorization' => 'applications/oauthserver/storage/PhabricatorOAuthClientAuthorization.php',
     'PhabricatorOAuthClientAuthorizationQuery' => 'applications/oauthserver/query/PhabricatorOAuthClientAuthorizationQuery.php',
     'PhabricatorOAuthClientController' => 'applications/oauthserver/controller/client/PhabricatorOAuthClientController.php',
     'PhabricatorOAuthClientDeleteController' => 'applications/oauthserver/controller/client/PhabricatorOAuthClientDeleteController.php',
     'PhabricatorOAuthClientEditController' => 'applications/oauthserver/controller/client/PhabricatorOAuthClientEditController.php',
     'PhabricatorOAuthClientListController' => 'applications/oauthserver/controller/client/PhabricatorOAuthClientListController.php',
     'PhabricatorOAuthClientSecretController' => 'applications/oauthserver/controller/client/PhabricatorOAuthClientSecretController.php',
     'PhabricatorOAuthClientViewController' => 'applications/oauthserver/controller/client/PhabricatorOAuthClientViewController.php',
     'PhabricatorOAuthResponse' => 'applications/oauthserver/PhabricatorOAuthResponse.php',
     'PhabricatorOAuthServer' => 'applications/oauthserver/PhabricatorOAuthServer.php',
     'PhabricatorOAuthServerAccessToken' => 'applications/oauthserver/storage/PhabricatorOAuthServerAccessToken.php',
     'PhabricatorOAuthServerApplication' => 'applications/oauthserver/application/PhabricatorOAuthServerApplication.php',
     'PhabricatorOAuthServerAuthController' => 'applications/oauthserver/controller/PhabricatorOAuthServerAuthController.php',
     'PhabricatorOAuthServerAuthorizationCode' => 'applications/oauthserver/storage/PhabricatorOAuthServerAuthorizationCode.php',
     'PhabricatorOAuthServerAuthorizationsSettingsPanel' => 'applications/oauthserver/panel/PhabricatorOAuthServerAuthorizationsSettingsPanel.php',
     'PhabricatorOAuthServerClient' => 'applications/oauthserver/storage/PhabricatorOAuthServerClient.php',
     'PhabricatorOAuthServerClientAuthorizationPHIDType' => 'applications/oauthserver/phid/PhabricatorOAuthServerClientAuthorizationPHIDType.php',
     'PhabricatorOAuthServerClientPHIDType' => 'applications/oauthserver/phid/PhabricatorOAuthServerClientPHIDType.php',
     'PhabricatorOAuthServerClientQuery' => 'applications/oauthserver/query/PhabricatorOAuthServerClientQuery.php',
     'PhabricatorOAuthServerClientSearchEngine' => 'applications/oauthserver/query/PhabricatorOAuthServerClientSearchEngine.php',
     'PhabricatorOAuthServerController' => 'applications/oauthserver/controller/PhabricatorOAuthServerController.php',
     'PhabricatorOAuthServerCreateClientsCapability' => 'applications/oauthserver/capability/PhabricatorOAuthServerCreateClientsCapability.php',
     'PhabricatorOAuthServerDAO' => 'applications/oauthserver/storage/PhabricatorOAuthServerDAO.php',
     'PhabricatorOAuthServerScope' => 'applications/oauthserver/PhabricatorOAuthServerScope.php',
     'PhabricatorOAuthServerTestCase' => 'applications/oauthserver/__tests__/PhabricatorOAuthServerTestCase.php',
     'PhabricatorOAuthServerTestController' => 'applications/oauthserver/controller/PhabricatorOAuthServerTestController.php',
     'PhabricatorOAuthServerTokenController' => 'applications/oauthserver/controller/PhabricatorOAuthServerTokenController.php',
     'PhabricatorObjectHandle' => 'applications/phid/PhabricatorObjectHandle.php',
     'PhabricatorObjectHasAsanaSubtaskEdgeType' => 'applications/doorkeeper/edge/PhabricatorObjectHasAsanaSubtaskEdgeType.php',
     'PhabricatorObjectHasAsanaTaskEdgeType' => 'applications/doorkeeper/edge/PhabricatorObjectHasAsanaTaskEdgeType.php',
     'PhabricatorObjectHasContributorEdgeType' => 'applications/transactions/edges/PhabricatorObjectHasContributorEdgeType.php',
     'PhabricatorObjectHasFileEdgeType' => 'applications/transactions/edges/PhabricatorObjectHasFileEdgeType.php',
     'PhabricatorObjectHasJiraIssueEdgeType' => 'applications/doorkeeper/edge/PhabricatorObjectHasJiraIssueEdgeType.php',
     'PhabricatorObjectHasSubscriberEdgeType' => 'applications/transactions/edges/PhabricatorObjectHasSubscriberEdgeType.php',
     'PhabricatorObjectHasUnsubscriberEdgeType' => 'applications/transactions/edges/PhabricatorObjectHasUnsubscriberEdgeType.php',
     'PhabricatorObjectHasWatcherEdgeType' => 'applications/transactions/edges/PhabricatorObjectHasWatcherEdgeType.php',
     'PhabricatorObjectListQuery' => 'applications/phid/query/PhabricatorObjectListQuery.php',
     'PhabricatorObjectListQueryTestCase' => 'applications/phid/query/__tests__/PhabricatorObjectListQueryTestCase.php',
     'PhabricatorObjectMailReceiver' => 'applications/metamta/receiver/PhabricatorObjectMailReceiver.php',
     'PhabricatorObjectMailReceiverTestCase' => 'applications/metamta/receiver/__tests__/PhabricatorObjectMailReceiverTestCase.php',
     'PhabricatorObjectMentionedByObjectEdgeType' => 'applications/transactions/edges/PhabricatorObjectMentionedByObjectEdgeType.php',
     'PhabricatorObjectMentionsObjectEdgeType' => 'applications/transactions/edges/PhabricatorObjectMentionsObjectEdgeType.php',
     'PhabricatorObjectQuery' => 'applications/phid/query/PhabricatorObjectQuery.php',
     'PhabricatorObjectRemarkupRule' => 'infrastructure/markup/rule/PhabricatorObjectRemarkupRule.php',
     'PhabricatorObjectSelectorDialog' => 'view/control/PhabricatorObjectSelectorDialog.php',
     'PhabricatorObjectUsesCredentialsEdgeType' => 'applications/transactions/edges/PhabricatorObjectUsesCredentialsEdgeType.php',
     'PhabricatorOffsetPagedQuery' => 'infrastructure/query/PhabricatorOffsetPagedQuery.php',
     'PhabricatorOneTimeTriggerClock' => 'infrastructure/daemon/workers/clock/PhabricatorOneTimeTriggerClock.php',
     'PhabricatorOpcodeCacheSpec' => 'applications/cache/spec/PhabricatorOpcodeCacheSpec.php',
     'PhabricatorOwnerPathQuery' => 'applications/owners/query/PhabricatorOwnerPathQuery.php',
     'PhabricatorOwnersApplication' => 'applications/owners/application/PhabricatorOwnersApplication.php',
     'PhabricatorOwnersConfigOptions' => 'applications/owners/config/PhabricatorOwnersConfigOptions.php',
     'PhabricatorOwnersController' => 'applications/owners/controller/PhabricatorOwnersController.php',
     'PhabricatorOwnersDAO' => 'applications/owners/storage/PhabricatorOwnersDAO.php',
     'PhabricatorOwnersDetailController' => 'applications/owners/controller/PhabricatorOwnersDetailController.php',
     'PhabricatorOwnersEditController' => 'applications/owners/controller/PhabricatorOwnersEditController.php',
     'PhabricatorOwnersListController' => 'applications/owners/controller/PhabricatorOwnersListController.php',
     'PhabricatorOwnersOwner' => 'applications/owners/storage/PhabricatorOwnersOwner.php',
     'PhabricatorOwnersPackage' => 'applications/owners/storage/PhabricatorOwnersPackage.php',
     'PhabricatorOwnersPackageDatasource' => 'applications/owners/typeahead/PhabricatorOwnersPackageDatasource.php',
     'PhabricatorOwnersPackagePHIDType' => 'applications/owners/phid/PhabricatorOwnersPackagePHIDType.php',
     'PhabricatorOwnersPackageQuery' => 'applications/owners/query/PhabricatorOwnersPackageQuery.php',
     'PhabricatorOwnersPackageSearchEngine' => 'applications/owners/query/PhabricatorOwnersPackageSearchEngine.php',
     'PhabricatorOwnersPackageTestCase' => 'applications/owners/storage/__tests__/PhabricatorOwnersPackageTestCase.php',
     'PhabricatorOwnersPackageTransaction' => 'applications/owners/storage/PhabricatorOwnersPackageTransaction.php',
     'PhabricatorOwnersPackageTransactionEditor' => 'applications/owners/editor/PhabricatorOwnersPackageTransactionEditor.php',
     'PhabricatorOwnersPackageTransactionQuery' => 'applications/owners/query/PhabricatorOwnersPackageTransactionQuery.php',
     'PhabricatorOwnersPath' => 'applications/owners/storage/PhabricatorOwnersPath.php',
     'PhabricatorOwnersPathsController' => 'applications/owners/controller/PhabricatorOwnersPathsController.php',
     'PhabricatorPHDConfigOptions' => 'applications/config/option/PhabricatorPHDConfigOptions.php',
     'PhabricatorPHID' => 'applications/phid/storage/PhabricatorPHID.php',
     'PhabricatorPHIDConstants' => 'applications/phid/PhabricatorPHIDConstants.php',
     'PhabricatorPHIDInterface' => 'applications/phid/interface/PhabricatorPHIDInterface.php',
     'PhabricatorPHIDType' => 'applications/phid/type/PhabricatorPHIDType.php',
+    'PhabricatorPHIDTypeTestCase' => 'applications/phid/type/__tests__/PhabricatorPHIDTypeTestCase.php',
     'PhabricatorPHPASTApplication' => 'applications/phpast/application/PhabricatorPHPASTApplication.php',
     'PhabricatorPHPConfigSetupCheck' => 'applications/config/check/PhabricatorPHPConfigSetupCheck.php',
     'PhabricatorPHPMailerConfigOptions' => 'applications/config/option/PhabricatorPHPMailerConfigOptions.php',
     'PhabricatorPagedFormUIExample' => 'applications/uiexample/examples/PhabricatorPagedFormUIExample.php',
     'PhabricatorPagerUIExample' => 'applications/uiexample/examples/PhabricatorPagerUIExample.php',
     'PhabricatorPassphraseApplication' => 'applications/passphrase/application/PhabricatorPassphraseApplication.php',
     'PhabricatorPasswordAuthProvider' => 'applications/auth/provider/PhabricatorPasswordAuthProvider.php',
     'PhabricatorPasswordHasher' => 'infrastructure/util/password/PhabricatorPasswordHasher.php',
     'PhabricatorPasswordHasherTestCase' => 'infrastructure/util/password/__tests__/PhabricatorPasswordHasherTestCase.php',
     'PhabricatorPasswordHasherUnavailableException' => 'infrastructure/util/password/PhabricatorPasswordHasherUnavailableException.php',
     'PhabricatorPasswordSettingsPanel' => 'applications/settings/panel/PhabricatorPasswordSettingsPanel.php',
     'PhabricatorPaste' => 'applications/paste/storage/PhabricatorPaste.php',
     'PhabricatorPasteApplication' => 'applications/paste/application/PhabricatorPasteApplication.php',
     'PhabricatorPasteCommentController' => 'applications/paste/controller/PhabricatorPasteCommentController.php',
     'PhabricatorPasteConfigOptions' => 'applications/paste/config/PhabricatorPasteConfigOptions.php',
     'PhabricatorPasteController' => 'applications/paste/controller/PhabricatorPasteController.php',
     'PhabricatorPasteDAO' => 'applications/paste/storage/PhabricatorPasteDAO.php',
     'PhabricatorPasteEditController' => 'applications/paste/controller/PhabricatorPasteEditController.php',
     'PhabricatorPasteEditor' => 'applications/paste/editor/PhabricatorPasteEditor.php',
     'PhabricatorPasteListController' => 'applications/paste/controller/PhabricatorPasteListController.php',
     'PhabricatorPastePastePHIDType' => 'applications/paste/phid/PhabricatorPastePastePHIDType.php',
     'PhabricatorPasteQuery' => 'applications/paste/query/PhabricatorPasteQuery.php',
     'PhabricatorPasteRemarkupRule' => 'applications/paste/remarkup/PhabricatorPasteRemarkupRule.php',
     'PhabricatorPasteSchemaSpec' => 'applications/paste/storage/PhabricatorPasteSchemaSpec.php',
     'PhabricatorPasteSearchEngine' => 'applications/paste/query/PhabricatorPasteSearchEngine.php',
     'PhabricatorPasteTestDataGenerator' => 'applications/paste/lipsum/PhabricatorPasteTestDataGenerator.php',
     'PhabricatorPasteTransaction' => 'applications/paste/storage/PhabricatorPasteTransaction.php',
     'PhabricatorPasteTransactionComment' => 'applications/paste/storage/PhabricatorPasteTransactionComment.php',
     'PhabricatorPasteTransactionQuery' => 'applications/paste/query/PhabricatorPasteTransactionQuery.php',
     'PhabricatorPasteViewController' => 'applications/paste/controller/PhabricatorPasteViewController.php',
     'PhabricatorPathSetupCheck' => 'applications/config/check/PhabricatorPathSetupCheck.php',
     'PhabricatorPeopleAnyOwnerDatasource' => 'applications/people/typeahead/PhabricatorPeopleAnyOwnerDatasource.php',
     'PhabricatorPeopleApplication' => 'applications/people/application/PhabricatorPeopleApplication.php',
     'PhabricatorPeopleApproveController' => 'applications/people/controller/PhabricatorPeopleApproveController.php',
     'PhabricatorPeopleCalendarController' => 'applications/people/controller/PhabricatorPeopleCalendarController.php',
     'PhabricatorPeopleController' => 'applications/people/controller/PhabricatorPeopleController.php',
     'PhabricatorPeopleCreateController' => 'applications/people/controller/PhabricatorPeopleCreateController.php',
     'PhabricatorPeopleDatasource' => 'applications/people/typeahead/PhabricatorPeopleDatasource.php',
     'PhabricatorPeopleDeleteController' => 'applications/people/controller/PhabricatorPeopleDeleteController.php',
     'PhabricatorPeopleDisableController' => 'applications/people/controller/PhabricatorPeopleDisableController.php',
     'PhabricatorPeopleEmpowerController' => 'applications/people/controller/PhabricatorPeopleEmpowerController.php',
     'PhabricatorPeopleExternalPHIDType' => 'applications/people/phid/PhabricatorPeopleExternalPHIDType.php',
     'PhabricatorPeopleHovercardEventListener' => 'applications/people/event/PhabricatorPeopleHovercardEventListener.php',
     'PhabricatorPeopleInviteController' => 'applications/people/controller/PhabricatorPeopleInviteController.php',
     'PhabricatorPeopleInviteListController' => 'applications/people/controller/PhabricatorPeopleInviteListController.php',
     'PhabricatorPeopleInviteSendController' => 'applications/people/controller/PhabricatorPeopleInviteSendController.php',
     'PhabricatorPeopleLdapController' => 'applications/people/controller/PhabricatorPeopleLdapController.php',
     'PhabricatorPeopleListController' => 'applications/people/controller/PhabricatorPeopleListController.php',
     'PhabricatorPeopleLogQuery' => 'applications/people/query/PhabricatorPeopleLogQuery.php',
     'PhabricatorPeopleLogSearchEngine' => 'applications/people/query/PhabricatorPeopleLogSearchEngine.php',
     'PhabricatorPeopleLogsController' => 'applications/people/controller/PhabricatorPeopleLogsController.php',
     'PhabricatorPeopleNewController' => 'applications/people/controller/PhabricatorPeopleNewController.php',
     'PhabricatorPeopleNoOwnerDatasource' => 'applications/people/typeahead/PhabricatorPeopleNoOwnerDatasource.php',
     'PhabricatorPeopleOwnerDatasource' => 'applications/people/typeahead/PhabricatorPeopleOwnerDatasource.php',
     'PhabricatorPeopleProfileController' => 'applications/people/controller/PhabricatorPeopleProfileController.php',
     'PhabricatorPeopleProfileEditController' => 'applications/people/controller/PhabricatorPeopleProfileEditController.php',
     'PhabricatorPeopleProfilePictureController' => 'applications/people/controller/PhabricatorPeopleProfilePictureController.php',
     'PhabricatorPeopleQuery' => 'applications/people/query/PhabricatorPeopleQuery.php',
     'PhabricatorPeopleRenameController' => 'applications/people/controller/PhabricatorPeopleRenameController.php',
     'PhabricatorPeopleSearchEngine' => 'applications/people/query/PhabricatorPeopleSearchEngine.php',
     'PhabricatorPeopleTestDataGenerator' => 'applications/people/lipsum/PhabricatorPeopleTestDataGenerator.php',
     'PhabricatorPeopleTransactionQuery' => 'applications/people/query/PhabricatorPeopleTransactionQuery.php',
     'PhabricatorPeopleUserFunctionDatasource' => 'applications/people/typeahead/PhabricatorPeopleUserFunctionDatasource.php',
     'PhabricatorPeopleUserPHIDType' => 'applications/people/phid/PhabricatorPeopleUserPHIDType.php',
     'PhabricatorPeopleWelcomeController' => 'applications/people/controller/PhabricatorPeopleWelcomeController.php',
     'PhabricatorPersonaAuthProvider' => 'applications/auth/provider/PhabricatorPersonaAuthProvider.php',
     'PhabricatorPhabricatorAuthProvider' => 'applications/auth/provider/PhabricatorPhabricatorAuthProvider.php',
     'PhabricatorPhameApplication' => 'applications/phame/application/PhabricatorPhameApplication.php',
     'PhabricatorPhameBlogPHIDType' => 'applications/phame/phid/PhabricatorPhameBlogPHIDType.php',
     'PhabricatorPhameConfigOptions' => 'applications/phame/config/PhabricatorPhameConfigOptions.php',
     'PhabricatorPhamePostPHIDType' => 'applications/phame/phid/PhabricatorPhamePostPHIDType.php',
     'PhabricatorPhluxApplication' => 'applications/phlux/application/PhabricatorPhluxApplication.php',
     'PhabricatorPholioApplication' => 'applications/pholio/application/PhabricatorPholioApplication.php',
     'PhabricatorPholioConfigOptions' => 'applications/pholio/config/PhabricatorPholioConfigOptions.php',
     'PhabricatorPholioMockTestDataGenerator' => 'applications/pholio/lipsum/PhabricatorPholioMockTestDataGenerator.php',
     'PhabricatorPhortuneApplication' => 'applications/phortune/application/PhabricatorPhortuneApplication.php',
     'PhabricatorPhortuneManagementInvoiceWorkflow' => 'applications/phortune/management/PhabricatorPhortuneManagementInvoiceWorkflow.php',
     'PhabricatorPhortuneManagementWorkflow' => 'applications/phortune/management/PhabricatorPhortuneManagementWorkflow.php',
     'PhabricatorPhragmentApplication' => 'applications/phragment/application/PhabricatorPhragmentApplication.php',
     'PhabricatorPhrequentApplication' => 'applications/phrequent/application/PhabricatorPhrequentApplication.php',
     'PhabricatorPhrequentConfigOptions' => 'applications/phrequent/config/PhabricatorPhrequentConfigOptions.php',
     'PhabricatorPhrictionApplication' => 'applications/phriction/application/PhabricatorPhrictionApplication.php',
     'PhabricatorPhrictionConfigOptions' => 'applications/phriction/config/PhabricatorPhrictionConfigOptions.php',
     'PhabricatorPolicies' => 'applications/policy/constants/PhabricatorPolicies.php',
     'PhabricatorPolicy' => 'applications/policy/storage/PhabricatorPolicy.php',
     'PhabricatorPolicyApplication' => 'applications/policy/application/PhabricatorPolicyApplication.php',
     'PhabricatorPolicyAwareQuery' => 'infrastructure/query/policy/PhabricatorPolicyAwareQuery.php',
     'PhabricatorPolicyAwareTestQuery' => 'applications/policy/__tests__/PhabricatorPolicyAwareTestQuery.php',
     'PhabricatorPolicyCanEditCapability' => 'applications/policy/capability/PhabricatorPolicyCanEditCapability.php',
     'PhabricatorPolicyCanJoinCapability' => 'applications/policy/capability/PhabricatorPolicyCanJoinCapability.php',
     'PhabricatorPolicyCanViewCapability' => 'applications/policy/capability/PhabricatorPolicyCanViewCapability.php',
     'PhabricatorPolicyCapability' => 'applications/policy/capability/PhabricatorPolicyCapability.php',
+    'PhabricatorPolicyCapabilityTestCase' => 'applications/policy/capability/__tests__/PhabricatorPolicyCapabilityTestCase.php',
     'PhabricatorPolicyConfigOptions' => 'applications/policy/config/PhabricatorPolicyConfigOptions.php',
     'PhabricatorPolicyConstants' => 'applications/policy/constants/PhabricatorPolicyConstants.php',
     'PhabricatorPolicyController' => 'applications/policy/controller/PhabricatorPolicyController.php',
     'PhabricatorPolicyDAO' => 'applications/policy/storage/PhabricatorPolicyDAO.php',
     'PhabricatorPolicyDataTestCase' => 'applications/policy/__tests__/PhabricatorPolicyDataTestCase.php',
     'PhabricatorPolicyEditController' => 'applications/policy/controller/PhabricatorPolicyEditController.php',
     'PhabricatorPolicyException' => 'applications/policy/exception/PhabricatorPolicyException.php',
     'PhabricatorPolicyExplainController' => 'applications/policy/controller/PhabricatorPolicyExplainController.php',
     'PhabricatorPolicyFilter' => 'applications/policy/filter/PhabricatorPolicyFilter.php',
     'PhabricatorPolicyInterface' => 'applications/policy/interface/PhabricatorPolicyInterface.php',
     'PhabricatorPolicyManagementShowWorkflow' => 'applications/policy/management/PhabricatorPolicyManagementShowWorkflow.php',
     'PhabricatorPolicyManagementUnlockWorkflow' => 'applications/policy/management/PhabricatorPolicyManagementUnlockWorkflow.php',
     'PhabricatorPolicyManagementWorkflow' => 'applications/policy/management/PhabricatorPolicyManagementWorkflow.php',
     'PhabricatorPolicyPHIDTypePolicy' => 'applications/policy/phid/PhabricatorPolicyPHIDTypePolicy.php',
     'PhabricatorPolicyQuery' => 'applications/policy/query/PhabricatorPolicyQuery.php',
     'PhabricatorPolicyRule' => 'applications/policy/rule/PhabricatorPolicyRule.php',
     'PhabricatorPolicyTestCase' => 'applications/policy/__tests__/PhabricatorPolicyTestCase.php',
     'PhabricatorPolicyTestObject' => 'applications/policy/__tests__/PhabricatorPolicyTestObject.php',
     'PhabricatorPolicyType' => 'applications/policy/constants/PhabricatorPolicyType.php',
     'PhabricatorPonderApplication' => 'applications/ponder/application/PhabricatorPonderApplication.php',
     'PhabricatorProject' => 'applications/project/storage/PhabricatorProject.php',
     'PhabricatorProjectApplication' => 'applications/project/application/PhabricatorProjectApplication.php',
     'PhabricatorProjectArchiveController' => 'applications/project/controller/PhabricatorProjectArchiveController.php',
     'PhabricatorProjectBoardController' => 'applications/project/controller/PhabricatorProjectBoardController.php',
     'PhabricatorProjectBoardImportController' => 'applications/project/controller/PhabricatorProjectBoardImportController.php',
     'PhabricatorProjectBoardReorderController' => 'applications/project/controller/PhabricatorProjectBoardReorderController.php',
     'PhabricatorProjectBoardViewController' => 'applications/project/controller/PhabricatorProjectBoardViewController.php',
     'PhabricatorProjectColumn' => 'applications/project/storage/PhabricatorProjectColumn.php',
     'PhabricatorProjectColumnDetailController' => 'applications/project/controller/PhabricatorProjectColumnDetailController.php',
     'PhabricatorProjectColumnEditController' => 'applications/project/controller/PhabricatorProjectColumnEditController.php',
     'PhabricatorProjectColumnHideController' => 'applications/project/controller/PhabricatorProjectColumnHideController.php',
     'PhabricatorProjectColumnPHIDType' => 'applications/project/phid/PhabricatorProjectColumnPHIDType.php',
     'PhabricatorProjectColumnPosition' => 'applications/project/storage/PhabricatorProjectColumnPosition.php',
     'PhabricatorProjectColumnPositionQuery' => 'applications/project/query/PhabricatorProjectColumnPositionQuery.php',
     'PhabricatorProjectColumnQuery' => 'applications/project/query/PhabricatorProjectColumnQuery.php',
     'PhabricatorProjectColumnTransaction' => 'applications/project/storage/PhabricatorProjectColumnTransaction.php',
     'PhabricatorProjectColumnTransactionEditor' => 'applications/project/editor/PhabricatorProjectColumnTransactionEditor.php',
     'PhabricatorProjectColumnTransactionQuery' => 'applications/project/query/PhabricatorProjectColumnTransactionQuery.php',
     'PhabricatorProjectConfigOptions' => 'applications/project/config/PhabricatorProjectConfigOptions.php',
     'PhabricatorProjectConfiguredCustomField' => 'applications/project/customfield/PhabricatorProjectConfiguredCustomField.php',
     'PhabricatorProjectController' => 'applications/project/controller/PhabricatorProjectController.php',
     'PhabricatorProjectCustomField' => 'applications/project/customfield/PhabricatorProjectCustomField.php',
     'PhabricatorProjectCustomFieldNumericIndex' => 'applications/project/storage/PhabricatorProjectCustomFieldNumericIndex.php',
     'PhabricatorProjectCustomFieldStorage' => 'applications/project/storage/PhabricatorProjectCustomFieldStorage.php',
     'PhabricatorProjectCustomFieldStringIndex' => 'applications/project/storage/PhabricatorProjectCustomFieldStringIndex.php',
     'PhabricatorProjectDAO' => 'applications/project/storage/PhabricatorProjectDAO.php',
     'PhabricatorProjectDatasource' => 'applications/project/typeahead/PhabricatorProjectDatasource.php',
     'PhabricatorProjectDescriptionField' => 'applications/project/customfield/PhabricatorProjectDescriptionField.php',
     'PhabricatorProjectEditDetailsController' => 'applications/project/controller/PhabricatorProjectEditDetailsController.php',
     'PhabricatorProjectEditIconController' => 'applications/project/controller/PhabricatorProjectEditIconController.php',
     'PhabricatorProjectEditPictureController' => 'applications/project/controller/PhabricatorProjectEditPictureController.php',
     'PhabricatorProjectEditorTestCase' => 'applications/project/editor/__tests__/PhabricatorProjectEditorTestCase.php',
     'PhabricatorProjectFeedController' => 'applications/project/controller/PhabricatorProjectFeedController.php',
     'PhabricatorProjectIcon' => 'applications/project/icon/PhabricatorProjectIcon.php',
     'PhabricatorProjectInterface' => 'applications/project/interface/PhabricatorProjectInterface.php',
     'PhabricatorProjectListController' => 'applications/project/controller/PhabricatorProjectListController.php',
     'PhabricatorProjectLogicalAndDatasource' => 'applications/project/typeahead/PhabricatorProjectLogicalAndDatasource.php',
     'PhabricatorProjectLogicalDatasource' => 'applications/project/typeahead/PhabricatorProjectLogicalDatasource.php',
     'PhabricatorProjectLogicalOrNotDatasource' => 'applications/project/typeahead/PhabricatorProjectLogicalOrNotDatasource.php',
     'PhabricatorProjectLogicalUserDatasource' => 'applications/project/typeahead/PhabricatorProjectLogicalUserDatasource.php',
     'PhabricatorProjectLogicalViewerDatasource' => 'applications/project/typeahead/PhabricatorProjectLogicalViewerDatasource.php',
     'PhabricatorProjectMemberOfProjectEdgeType' => 'applications/project/edge/PhabricatorProjectMemberOfProjectEdgeType.php',
     'PhabricatorProjectMembersDatasource' => 'applications/project/typeahead/PhabricatorProjectMembersDatasource.php',
     'PhabricatorProjectMembersEditController' => 'applications/project/controller/PhabricatorProjectMembersEditController.php',
     'PhabricatorProjectMembersRemoveController' => 'applications/project/controller/PhabricatorProjectMembersRemoveController.php',
     'PhabricatorProjectMoveController' => 'applications/project/controller/PhabricatorProjectMoveController.php',
     'PhabricatorProjectNoProjectsDatasource' => 'applications/project/typeahead/PhabricatorProjectNoProjectsDatasource.php',
     'PhabricatorProjectObjectHasProjectEdgeType' => 'applications/project/edge/PhabricatorProjectObjectHasProjectEdgeType.php',
     'PhabricatorProjectOrUserDatasource' => 'applications/project/typeahead/PhabricatorProjectOrUserDatasource.php',
     'PhabricatorProjectProfileController' => 'applications/project/controller/PhabricatorProjectProfileController.php',
     'PhabricatorProjectProjectHasMemberEdgeType' => 'applications/project/edge/PhabricatorProjectProjectHasMemberEdgeType.php',
     'PhabricatorProjectProjectHasObjectEdgeType' => 'applications/project/edge/PhabricatorProjectProjectHasObjectEdgeType.php',
     'PhabricatorProjectProjectPHIDType' => 'applications/project/phid/PhabricatorProjectProjectPHIDType.php',
     'PhabricatorProjectQuery' => 'applications/project/query/PhabricatorProjectQuery.php',
     'PhabricatorProjectSchemaSpec' => 'applications/project/storage/PhabricatorProjectSchemaSpec.php',
     'PhabricatorProjectSearchEngine' => 'applications/project/query/PhabricatorProjectSearchEngine.php',
     'PhabricatorProjectSearchIndexer' => 'applications/project/search/PhabricatorProjectSearchIndexer.php',
     'PhabricatorProjectSlug' => 'applications/project/storage/PhabricatorProjectSlug.php',
     'PhabricatorProjectStandardCustomField' => 'applications/project/customfield/PhabricatorProjectStandardCustomField.php',
     'PhabricatorProjectStatus' => 'applications/project/constants/PhabricatorProjectStatus.php',
     'PhabricatorProjectTestDataGenerator' => 'applications/project/lipsum/PhabricatorProjectTestDataGenerator.php',
     'PhabricatorProjectTransaction' => 'applications/project/storage/PhabricatorProjectTransaction.php',
     'PhabricatorProjectTransactionEditor' => 'applications/project/editor/PhabricatorProjectTransactionEditor.php',
     'PhabricatorProjectTransactionQuery' => 'applications/project/query/PhabricatorProjectTransactionQuery.php',
     'PhabricatorProjectUIEventListener' => 'applications/project/events/PhabricatorProjectUIEventListener.php',
     'PhabricatorProjectUpdateController' => 'applications/project/controller/PhabricatorProjectUpdateController.php',
     'PhabricatorProjectViewController' => 'applications/project/controller/PhabricatorProjectViewController.php',
     'PhabricatorProjectWatchController' => 'applications/project/controller/PhabricatorProjectWatchController.php',
     'PhabricatorProjectsPolicyRule' => 'applications/policy/rule/PhabricatorProjectsPolicyRule.php',
     'PhabricatorProtocolAdapter' => 'infrastructure/daemon/bot/adapter/PhabricatorProtocolAdapter.php',
     'PhabricatorPygmentSetupCheck' => 'applications/config/check/PhabricatorPygmentSetupCheck.php',
     'PhabricatorQuery' => 'infrastructure/query/PhabricatorQuery.php',
     'PhabricatorQueryConstraint' => 'infrastructure/query/constraint/PhabricatorQueryConstraint.php',
     'PhabricatorQueryOrderItem' => 'infrastructure/query/order/PhabricatorQueryOrderItem.php',
     'PhabricatorQueryOrderTestCase' => 'infrastructure/query/order/__tests__/PhabricatorQueryOrderTestCase.php',
     'PhabricatorQueryOrderVector' => 'infrastructure/query/order/PhabricatorQueryOrderVector.php',
     'PhabricatorRecaptchaConfigOptions' => 'applications/config/option/PhabricatorRecaptchaConfigOptions.php',
     'PhabricatorRedirectController' => 'applications/base/controller/PhabricatorRedirectController.php',
     'PhabricatorRefreshCSRFController' => 'applications/auth/controller/PhabricatorRefreshCSRFController.php',
     'PhabricatorRegistrationProfile' => 'applications/people/storage/PhabricatorRegistrationProfile.php',
     'PhabricatorReleephApplication' => 'applications/releeph/application/PhabricatorReleephApplication.php',
     'PhabricatorReleephApplicationConfigOptions' => 'applications/releeph/config/PhabricatorReleephApplicationConfigOptions.php',
     'PhabricatorRemarkupControl' => 'view/form/control/PhabricatorRemarkupControl.php',
     'PhabricatorRemarkupCowsayBlockInterpreter' => 'infrastructure/markup/interpreter/PhabricatorRemarkupCowsayBlockInterpreter.php',
     'PhabricatorRemarkupCustomBlockRule' => 'infrastructure/markup/rule/PhabricatorRemarkupCustomBlockRule.php',
     'PhabricatorRemarkupCustomInlineRule' => 'infrastructure/markup/rule/PhabricatorRemarkupCustomInlineRule.php',
     'PhabricatorRemarkupFigletBlockInterpreter' => 'infrastructure/markup/interpreter/PhabricatorRemarkupFigletBlockInterpreter.php',
     'PhabricatorRemarkupGraphvizBlockInterpreter' => 'infrastructure/markup/interpreter/PhabricatorRemarkupGraphvizBlockInterpreter.php',
     'PhabricatorRemarkupUIExample' => 'applications/uiexample/examples/PhabricatorRemarkupUIExample.php',
     'PhabricatorRepositoriesSetupCheck' => 'applications/config/check/PhabricatorRepositoriesSetupCheck.php',
     'PhabricatorRepository' => 'applications/repository/storage/PhabricatorRepository.php',
     'PhabricatorRepositoryArcanistProject' => 'applications/repository/storage/PhabricatorRepositoryArcanistProject.php',
     'PhabricatorRepositoryArcanistProjectPHIDType' => 'applications/repository/phid/PhabricatorRepositoryArcanistProjectPHIDType.php',
     'PhabricatorRepositoryArcanistProjectQuery' => 'applications/repository/query/PhabricatorRepositoryArcanistProjectQuery.php',
     'PhabricatorRepositoryAuditRequest' => 'applications/repository/storage/PhabricatorRepositoryAuditRequest.php',
     'PhabricatorRepositoryBranch' => 'applications/repository/storage/PhabricatorRepositoryBranch.php',
     'PhabricatorRepositoryCommit' => 'applications/repository/storage/PhabricatorRepositoryCommit.php',
     'PhabricatorRepositoryCommitChangeParserWorker' => 'applications/repository/worker/commitchangeparser/PhabricatorRepositoryCommitChangeParserWorker.php',
     'PhabricatorRepositoryCommitData' => 'applications/repository/storage/PhabricatorRepositoryCommitData.php',
     'PhabricatorRepositoryCommitHeraldWorker' => 'applications/repository/worker/PhabricatorRepositoryCommitHeraldWorker.php',
     'PhabricatorRepositoryCommitMessageParserWorker' => 'applications/repository/worker/commitmessageparser/PhabricatorRepositoryCommitMessageParserWorker.php',
     'PhabricatorRepositoryCommitOwnersWorker' => 'applications/repository/worker/PhabricatorRepositoryCommitOwnersWorker.php',
     'PhabricatorRepositoryCommitPHIDType' => 'applications/repository/phid/PhabricatorRepositoryCommitPHIDType.php',
     'PhabricatorRepositoryCommitParserWorker' => 'applications/repository/worker/PhabricatorRepositoryCommitParserWorker.php',
     'PhabricatorRepositoryCommitRef' => 'applications/repository/engine/PhabricatorRepositoryCommitRef.php',
     'PhabricatorRepositoryCommitSearchIndexer' => 'applications/repository/search/PhabricatorRepositoryCommitSearchIndexer.php',
     'PhabricatorRepositoryConfigOptions' => 'applications/repository/config/PhabricatorRepositoryConfigOptions.php',
     'PhabricatorRepositoryDAO' => 'applications/repository/storage/PhabricatorRepositoryDAO.php',
     'PhabricatorRepositoryDiscoveryEngine' => 'applications/repository/engine/PhabricatorRepositoryDiscoveryEngine.php',
     'PhabricatorRepositoryEditor' => 'applications/repository/editor/PhabricatorRepositoryEditor.php',
     'PhabricatorRepositoryEngine' => 'applications/repository/engine/PhabricatorRepositoryEngine.php',
     'PhabricatorRepositoryGitCommitChangeParserWorker' => 'applications/repository/worker/commitchangeparser/PhabricatorRepositoryGitCommitChangeParserWorker.php',
     'PhabricatorRepositoryGitCommitMessageParserWorker' => 'applications/repository/worker/commitmessageparser/PhabricatorRepositoryGitCommitMessageParserWorker.php',
     'PhabricatorRepositoryGraphCache' => 'applications/repository/graphcache/PhabricatorRepositoryGraphCache.php',
     'PhabricatorRepositoryGraphStream' => 'applications/repository/daemon/PhabricatorRepositoryGraphStream.php',
     'PhabricatorRepositoryManagementCacheWorkflow' => 'applications/repository/management/PhabricatorRepositoryManagementCacheWorkflow.php',
     'PhabricatorRepositoryManagementDiscoverWorkflow' => 'applications/repository/management/PhabricatorRepositoryManagementDiscoverWorkflow.php',
     'PhabricatorRepositoryManagementEditWorkflow' => 'applications/repository/management/PhabricatorRepositoryManagementEditWorkflow.php',
     'PhabricatorRepositoryManagementImportingWorkflow' => 'applications/repository/management/PhabricatorRepositoryManagementImportingWorkflow.php',
     'PhabricatorRepositoryManagementListWorkflow' => 'applications/repository/management/PhabricatorRepositoryManagementListWorkflow.php',
     'PhabricatorRepositoryManagementLookupUsersWorkflow' => 'applications/repository/management/PhabricatorRepositoryManagementLookupUsersWorkflow.php',
     'PhabricatorRepositoryManagementMarkImportedWorkflow' => 'applications/repository/management/PhabricatorRepositoryManagementMarkImportedWorkflow.php',
     'PhabricatorRepositoryManagementMirrorWorkflow' => 'applications/repository/management/PhabricatorRepositoryManagementMirrorWorkflow.php',
     'PhabricatorRepositoryManagementParentsWorkflow' => 'applications/repository/management/PhabricatorRepositoryManagementParentsWorkflow.php',
     'PhabricatorRepositoryManagementPullWorkflow' => 'applications/repository/management/PhabricatorRepositoryManagementPullWorkflow.php',
     'PhabricatorRepositoryManagementRefsWorkflow' => 'applications/repository/management/PhabricatorRepositoryManagementRefsWorkflow.php',
     'PhabricatorRepositoryManagementReparseWorkflow' => 'applications/repository/management/PhabricatorRepositoryManagementReparseWorkflow.php',
     'PhabricatorRepositoryManagementUpdateWorkflow' => 'applications/repository/management/PhabricatorRepositoryManagementUpdateWorkflow.php',
     'PhabricatorRepositoryManagementWorkflow' => 'applications/repository/management/PhabricatorRepositoryManagementWorkflow.php',
     'PhabricatorRepositoryMercurialCommitChangeParserWorker' => 'applications/repository/worker/commitchangeparser/PhabricatorRepositoryMercurialCommitChangeParserWorker.php',
     'PhabricatorRepositoryMercurialCommitMessageParserWorker' => 'applications/repository/worker/commitmessageparser/PhabricatorRepositoryMercurialCommitMessageParserWorker.php',
     'PhabricatorRepositoryMirror' => 'applications/repository/storage/PhabricatorRepositoryMirror.php',
     'PhabricatorRepositoryMirrorEngine' => 'applications/repository/engine/PhabricatorRepositoryMirrorEngine.php',
     'PhabricatorRepositoryMirrorPHIDType' => 'applications/repository/phid/PhabricatorRepositoryMirrorPHIDType.php',
     'PhabricatorRepositoryMirrorQuery' => 'applications/repository/query/PhabricatorRepositoryMirrorQuery.php',
     'PhabricatorRepositoryParsedChange' => 'applications/repository/data/PhabricatorRepositoryParsedChange.php',
     'PhabricatorRepositoryPullEngine' => 'applications/repository/engine/PhabricatorRepositoryPullEngine.php',
     'PhabricatorRepositoryPullLocalDaemon' => 'applications/repository/daemon/PhabricatorRepositoryPullLocalDaemon.php',
     'PhabricatorRepositoryPushEvent' => 'applications/repository/storage/PhabricatorRepositoryPushEvent.php',
     'PhabricatorRepositoryPushEventPHIDType' => 'applications/repository/phid/PhabricatorRepositoryPushEventPHIDType.php',
     'PhabricatorRepositoryPushEventQuery' => 'applications/repository/query/PhabricatorRepositoryPushEventQuery.php',
     'PhabricatorRepositoryPushLog' => 'applications/repository/storage/PhabricatorRepositoryPushLog.php',
     'PhabricatorRepositoryPushLogPHIDType' => 'applications/repository/phid/PhabricatorRepositoryPushLogPHIDType.php',
     'PhabricatorRepositoryPushLogQuery' => 'applications/repository/query/PhabricatorRepositoryPushLogQuery.php',
     'PhabricatorRepositoryPushLogSearchEngine' => 'applications/repository/query/PhabricatorRepositoryPushLogSearchEngine.php',
     'PhabricatorRepositoryPushMailWorker' => 'applications/repository/worker/PhabricatorRepositoryPushMailWorker.php',
     'PhabricatorRepositoryPushReplyHandler' => 'applications/repository/mail/PhabricatorRepositoryPushReplyHandler.php',
     'PhabricatorRepositoryQuery' => 'applications/repository/query/PhabricatorRepositoryQuery.php',
     'PhabricatorRepositoryRefCursor' => 'applications/repository/storage/PhabricatorRepositoryRefCursor.php',
     'PhabricatorRepositoryRefCursorQuery' => 'applications/repository/query/PhabricatorRepositoryRefCursorQuery.php',
     'PhabricatorRepositoryRefEngine' => 'applications/repository/engine/PhabricatorRepositoryRefEngine.php',
     'PhabricatorRepositoryRepositoryPHIDType' => 'applications/repository/phid/PhabricatorRepositoryRepositoryPHIDType.php',
     'PhabricatorRepositorySchemaSpec' => 'applications/repository/storage/PhabricatorRepositorySchemaSpec.php',
     'PhabricatorRepositorySearchEngine' => 'applications/repository/query/PhabricatorRepositorySearchEngine.php',
     'PhabricatorRepositoryStatusMessage' => 'applications/repository/storage/PhabricatorRepositoryStatusMessage.php',
     'PhabricatorRepositorySvnCommitChangeParserWorker' => 'applications/repository/worker/commitchangeparser/PhabricatorRepositorySvnCommitChangeParserWorker.php',
     'PhabricatorRepositorySvnCommitMessageParserWorker' => 'applications/repository/worker/commitmessageparser/PhabricatorRepositorySvnCommitMessageParserWorker.php',
     'PhabricatorRepositorySymbol' => 'applications/repository/storage/PhabricatorRepositorySymbol.php',
     'PhabricatorRepositoryTestCase' => 'applications/repository/storage/__tests__/PhabricatorRepositoryTestCase.php',
     'PhabricatorRepositoryTransaction' => 'applications/repository/storage/PhabricatorRepositoryTransaction.php',
     'PhabricatorRepositoryTransactionQuery' => 'applications/repository/query/PhabricatorRepositoryTransactionQuery.php',
     'PhabricatorRepositoryType' => 'applications/repository/constants/PhabricatorRepositoryType.php',
     'PhabricatorRepositoryURINormalizer' => 'applications/repository/data/PhabricatorRepositoryURINormalizer.php',
     'PhabricatorRepositoryURINormalizerTestCase' => 'applications/repository/data/__tests__/PhabricatorRepositoryURINormalizerTestCase.php',
     'PhabricatorRepositoryURITestCase' => 'applications/repository/storage/__tests__/PhabricatorRepositoryURITestCase.php',
     'PhabricatorRepositoryVCSPassword' => 'applications/repository/storage/PhabricatorRepositoryVCSPassword.php',
     'PhabricatorRepositoryVersion' => 'applications/repository/constants/PhabricatorRepositoryVersion.php',
     'PhabricatorRobotsController' => 'applications/system/controller/PhabricatorRobotsController.php',
     'PhabricatorS3FileStorageEngine' => 'applications/files/engine/PhabricatorS3FileStorageEngine.php',
     'PhabricatorSMS' => 'infrastructure/sms/storage/PhabricatorSMS.php',
     'PhabricatorSMSConfigOptions' => 'applications/config/option/PhabricatorSMSConfigOptions.php',
     'PhabricatorSMSDAO' => 'infrastructure/sms/storage/PhabricatorSMSDAO.php',
     'PhabricatorSMSDemultiplexWorker' => 'infrastructure/sms/worker/PhabricatorSMSDemultiplexWorker.php',
     'PhabricatorSMSImplementationAdapter' => 'infrastructure/sms/adapter/PhabricatorSMSImplementationAdapter.php',
     'PhabricatorSMSImplementationTestBlackholeAdapter' => 'infrastructure/sms/adapter/PhabricatorSMSImplementationTestBlackholeAdapter.php',
     'PhabricatorSMSImplementationTwilioAdapter' => 'infrastructure/sms/adapter/PhabricatorSMSImplementationTwilioAdapter.php',
     'PhabricatorSMSManagementListOutboundWorkflow' => 'infrastructure/sms/management/PhabricatorSMSManagementListOutboundWorkflow.php',
     'PhabricatorSMSManagementSendTestWorkflow' => 'infrastructure/sms/management/PhabricatorSMSManagementSendTestWorkflow.php',
     'PhabricatorSMSManagementShowOutboundWorkflow' => 'infrastructure/sms/management/PhabricatorSMSManagementShowOutboundWorkflow.php',
     'PhabricatorSMSManagementWorkflow' => 'infrastructure/sms/management/PhabricatorSMSManagementWorkflow.php',
     'PhabricatorSMSSendWorker' => 'infrastructure/sms/worker/PhabricatorSMSSendWorker.php',
     'PhabricatorSMSWorker' => 'infrastructure/sms/worker/PhabricatorSMSWorker.php',
     'PhabricatorSQLPatchList' => 'infrastructure/storage/patch/PhabricatorSQLPatchList.php',
     'PhabricatorSSHKeyGenerator' => 'infrastructure/util/PhabricatorSSHKeyGenerator.php',
     'PhabricatorSSHKeysSettingsPanel' => 'applications/settings/panel/PhabricatorSSHKeysSettingsPanel.php',
     'PhabricatorSSHLog' => 'infrastructure/log/PhabricatorSSHLog.php',
     'PhabricatorSSHPassthruCommand' => 'infrastructure/ssh/PhabricatorSSHPassthruCommand.php',
     'PhabricatorSSHPublicKeyInterface' => 'applications/auth/sshkey/PhabricatorSSHPublicKeyInterface.php',
     'PhabricatorSSHWorkflow' => 'infrastructure/ssh/PhabricatorSSHWorkflow.php',
     'PhabricatorSavedQuery' => 'applications/search/storage/PhabricatorSavedQuery.php',
     'PhabricatorSavedQueryQuery' => 'applications/search/query/PhabricatorSavedQueryQuery.php',
     'PhabricatorScheduleTaskTriggerAction' => 'infrastructure/daemon/workers/action/PhabricatorScheduleTaskTriggerAction.php',
     'PhabricatorScopedEnv' => 'infrastructure/env/PhabricatorScopedEnv.php',
     'PhabricatorSearchAbstractDocument' => 'applications/search/index/PhabricatorSearchAbstractDocument.php',
     'PhabricatorSearchApplication' => 'applications/search/application/PhabricatorSearchApplication.php',
     'PhabricatorSearchApplicationSearchEngine' => 'applications/search/query/PhabricatorSearchApplicationSearchEngine.php',
     'PhabricatorSearchApplicationStorageEnginePanel' => 'applications/search/applicationpanel/PhabricatorSearchApplicationStorageEnginePanel.php',
     'PhabricatorSearchAttachController' => 'applications/search/controller/PhabricatorSearchAttachController.php',
     'PhabricatorSearchBaseController' => 'applications/search/controller/PhabricatorSearchBaseController.php',
     'PhabricatorSearchCheckboxesField' => 'applications/search/field/PhabricatorSearchCheckboxesField.php',
     'PhabricatorSearchConfigOptions' => 'applications/search/config/PhabricatorSearchConfigOptions.php',
     'PhabricatorSearchController' => 'applications/search/controller/PhabricatorSearchController.php',
     'PhabricatorSearchCustomFieldProxyField' => 'applications/search/field/PhabricatorSearchCustomFieldProxyField.php',
     'PhabricatorSearchDAO' => 'applications/search/storage/PhabricatorSearchDAO.php',
     'PhabricatorSearchDatasource' => 'applications/search/typeahead/PhabricatorSearchDatasource.php',
     'PhabricatorSearchDatasourceField' => 'applications/search/field/PhabricatorSearchDatasourceField.php',
     'PhabricatorSearchDateField' => 'applications/search/field/PhabricatorSearchDateField.php',
     'PhabricatorSearchDeleteController' => 'applications/search/controller/PhabricatorSearchDeleteController.php',
     'PhabricatorSearchDocument' => 'applications/search/storage/document/PhabricatorSearchDocument.php',
     'PhabricatorSearchDocumentField' => 'applications/search/storage/document/PhabricatorSearchDocumentField.php',
     'PhabricatorSearchDocumentFieldType' => 'applications/search/constants/PhabricatorSearchDocumentFieldType.php',
     'PhabricatorSearchDocumentIndexer' => 'applications/search/index/PhabricatorSearchDocumentIndexer.php',
     'PhabricatorSearchDocumentQuery' => 'applications/search/query/PhabricatorSearchDocumentQuery.php',
     'PhabricatorSearchDocumentRelationship' => 'applications/search/storage/document/PhabricatorSearchDocumentRelationship.php',
     'PhabricatorSearchDocumentTypeDatasource' => 'applications/search/typeahead/PhabricatorSearchDocumentTypeDatasource.php',
     'PhabricatorSearchEditController' => 'applications/search/controller/PhabricatorSearchEditController.php',
     'PhabricatorSearchEngine' => 'applications/search/engine/PhabricatorSearchEngine.php',
+    'PhabricatorSearchEngineTestCase' => 'applications/search/engine/__tests__/PhabricatorSearchEngineTestCase.php',
     'PhabricatorSearchField' => 'applications/search/field/PhabricatorSearchField.php',
     'PhabricatorSearchHovercardController' => 'applications/search/controller/PhabricatorSearchHovercardController.php',
     'PhabricatorSearchIndexer' => 'applications/search/index/PhabricatorSearchIndexer.php',
     'PhabricatorSearchManagementIndexWorkflow' => 'applications/search/management/PhabricatorSearchManagementIndexWorkflow.php',
     'PhabricatorSearchManagementInitWorkflow' => 'applications/search/management/PhabricatorSearchManagementInitWorkflow.php',
     'PhabricatorSearchManagementWorkflow' => 'applications/search/management/PhabricatorSearchManagementWorkflow.php',
     'PhabricatorSearchOrderController' => 'applications/search/controller/PhabricatorSearchOrderController.php',
     'PhabricatorSearchOrderField' => 'applications/search/field/PhabricatorSearchOrderField.php',
     'PhabricatorSearchOwnersField' => 'applications/search/field/PhabricatorSearchOwnersField.php',
     'PhabricatorSearchPreferencesSettingsPanel' => 'applications/settings/panel/PhabricatorSearchPreferencesSettingsPanel.php',
     'PhabricatorSearchProjectsField' => 'applications/search/field/PhabricatorSearchProjectsField.php',
     'PhabricatorSearchRelationship' => 'applications/search/constants/PhabricatorSearchRelationship.php',
     'PhabricatorSearchResultView' => 'applications/search/view/PhabricatorSearchResultView.php',
     'PhabricatorSearchSelectController' => 'applications/search/controller/PhabricatorSearchSelectController.php',
     'PhabricatorSearchSelectField' => 'applications/search/field/PhabricatorSearchSelectField.php',
     'PhabricatorSearchSpacesField' => 'applications/search/field/PhabricatorSearchSpacesField.php',
     'PhabricatorSearchStringListField' => 'applications/search/field/PhabricatorSearchStringListField.php',
     'PhabricatorSearchSubscribersField' => 'applications/search/field/PhabricatorSearchSubscribersField.php',
     'PhabricatorSearchTextField' => 'applications/search/field/PhabricatorSearchTextField.php',
     'PhabricatorSearchThreeStateField' => 'applications/search/field/PhabricatorSearchThreeStateField.php',
     'PhabricatorSearchTokenizerField' => 'applications/search/field/PhabricatorSearchTokenizerField.php',
     'PhabricatorSearchUsersField' => 'applications/search/field/PhabricatorSearchUsersField.php',
     'PhabricatorSearchWorker' => 'applications/search/worker/PhabricatorSearchWorker.php',
     'PhabricatorSecurityConfigOptions' => 'applications/config/option/PhabricatorSecurityConfigOptions.php',
     'PhabricatorSecuritySetupCheck' => 'applications/config/check/PhabricatorSecuritySetupCheck.php',
     'PhabricatorSendGridConfigOptions' => 'applications/config/option/PhabricatorSendGridConfigOptions.php',
     'PhabricatorSessionsSettingsPanel' => 'applications/settings/panel/PhabricatorSessionsSettingsPanel.php',
     'PhabricatorSettingsAddEmailAction' => 'applications/settings/action/PhabricatorSettingsAddEmailAction.php',
     'PhabricatorSettingsAdjustController' => 'applications/settings/controller/PhabricatorSettingsAdjustController.php',
     'PhabricatorSettingsApplication' => 'applications/settings/application/PhabricatorSettingsApplication.php',
     'PhabricatorSettingsMainController' => 'applications/settings/controller/PhabricatorSettingsMainController.php',
     'PhabricatorSettingsPanel' => 'applications/settings/panel/PhabricatorSettingsPanel.php',
     'PhabricatorSetupCheck' => 'applications/config/check/PhabricatorSetupCheck.php',
+    'PhabricatorSetupCheckTestCase' => 'applications/config/check/__tests__/PhabricatorSetupCheckTestCase.php',
     'PhabricatorSetupIssue' => 'applications/config/issue/PhabricatorSetupIssue.php',
     'PhabricatorSetupIssueUIExample' => 'applications/uiexample/examples/PhabricatorSetupIssueUIExample.php',
     'PhabricatorSetupIssueView' => 'applications/config/view/PhabricatorSetupIssueView.php',
     'PhabricatorSlowvoteApplication' => 'applications/slowvote/application/PhabricatorSlowvoteApplication.php',
     'PhabricatorSlowvoteChoice' => 'applications/slowvote/storage/PhabricatorSlowvoteChoice.php',
     'PhabricatorSlowvoteCloseController' => 'applications/slowvote/controller/PhabricatorSlowvoteCloseController.php',
     'PhabricatorSlowvoteCommentController' => 'applications/slowvote/controller/PhabricatorSlowvoteCommentController.php',
     'PhabricatorSlowvoteController' => 'applications/slowvote/controller/PhabricatorSlowvoteController.php',
     'PhabricatorSlowvoteDAO' => 'applications/slowvote/storage/PhabricatorSlowvoteDAO.php',
     'PhabricatorSlowvoteDefaultViewCapability' => 'applications/slowvote/capability/PhabricatorSlowvoteDefaultViewCapability.php',
     'PhabricatorSlowvoteEditController' => 'applications/slowvote/controller/PhabricatorSlowvoteEditController.php',
     'PhabricatorSlowvoteEditor' => 'applications/slowvote/editor/PhabricatorSlowvoteEditor.php',
     'PhabricatorSlowvoteListController' => 'applications/slowvote/controller/PhabricatorSlowvoteListController.php',
     'PhabricatorSlowvoteOption' => 'applications/slowvote/storage/PhabricatorSlowvoteOption.php',
     'PhabricatorSlowvotePoll' => 'applications/slowvote/storage/PhabricatorSlowvotePoll.php',
     'PhabricatorSlowvotePollController' => 'applications/slowvote/controller/PhabricatorSlowvotePollController.php',
     'PhabricatorSlowvotePollPHIDType' => 'applications/slowvote/phid/PhabricatorSlowvotePollPHIDType.php',
     'PhabricatorSlowvoteQuery' => 'applications/slowvote/query/PhabricatorSlowvoteQuery.php',
     'PhabricatorSlowvoteSchemaSpec' => 'applications/slowvote/storage/PhabricatorSlowvoteSchemaSpec.php',
     'PhabricatorSlowvoteSearchEngine' => 'applications/slowvote/query/PhabricatorSlowvoteSearchEngine.php',
     'PhabricatorSlowvoteTransaction' => 'applications/slowvote/storage/PhabricatorSlowvoteTransaction.php',
     'PhabricatorSlowvoteTransactionComment' => 'applications/slowvote/storage/PhabricatorSlowvoteTransactionComment.php',
     'PhabricatorSlowvoteTransactionQuery' => 'applications/slowvote/query/PhabricatorSlowvoteTransactionQuery.php',
     'PhabricatorSlowvoteVoteController' => 'applications/slowvote/controller/PhabricatorSlowvoteVoteController.php',
     'PhabricatorSlug' => 'infrastructure/util/PhabricatorSlug.php',
     'PhabricatorSlugTestCase' => 'infrastructure/util/__tests__/PhabricatorSlugTestCase.php',
     'PhabricatorSortTableUIExample' => 'applications/uiexample/examples/PhabricatorSortTableUIExample.php',
     'PhabricatorSourceCodeView' => 'view/layout/PhabricatorSourceCodeView.php',
     'PhabricatorSpacesApplication' => 'applications/spaces/application/PhabricatorSpacesApplication.php',
+    'PhabricatorSpacesArchiveController' => 'applications/spaces/controller/PhabricatorSpacesArchiveController.php',
     'PhabricatorSpacesCapabilityCreateSpaces' => 'applications/spaces/capability/PhabricatorSpacesCapabilityCreateSpaces.php',
     'PhabricatorSpacesCapabilityDefaultEdit' => 'applications/spaces/capability/PhabricatorSpacesCapabilityDefaultEdit.php',
     'PhabricatorSpacesCapabilityDefaultView' => 'applications/spaces/capability/PhabricatorSpacesCapabilityDefaultView.php',
-    'PhabricatorSpacesControl' => 'applications/spaces/view/PhabricatorSpacesControl.php',
     'PhabricatorSpacesController' => 'applications/spaces/controller/PhabricatorSpacesController.php',
     'PhabricatorSpacesDAO' => 'applications/spaces/storage/PhabricatorSpacesDAO.php',
     'PhabricatorSpacesEditController' => 'applications/spaces/controller/PhabricatorSpacesEditController.php',
     'PhabricatorSpacesInterface' => 'applications/spaces/interface/PhabricatorSpacesInterface.php',
     'PhabricatorSpacesListController' => 'applications/spaces/controller/PhabricatorSpacesListController.php',
     'PhabricatorSpacesNamespace' => 'applications/spaces/storage/PhabricatorSpacesNamespace.php',
     'PhabricatorSpacesNamespaceDatasource' => 'applications/spaces/typeahead/PhabricatorSpacesNamespaceDatasource.php',
     'PhabricatorSpacesNamespaceEditor' => 'applications/spaces/editor/PhabricatorSpacesNamespaceEditor.php',
     'PhabricatorSpacesNamespacePHIDType' => 'applications/spaces/phid/PhabricatorSpacesNamespacePHIDType.php',
     'PhabricatorSpacesNamespaceQuery' => 'applications/spaces/query/PhabricatorSpacesNamespaceQuery.php',
     'PhabricatorSpacesNamespaceSearchEngine' => 'applications/spaces/query/PhabricatorSpacesNamespaceSearchEngine.php',
     'PhabricatorSpacesNamespaceTransaction' => 'applications/spaces/storage/PhabricatorSpacesNamespaceTransaction.php',
     'PhabricatorSpacesNamespaceTransactionQuery' => 'applications/spaces/query/PhabricatorSpacesNamespaceTransactionQuery.php',
     'PhabricatorSpacesRemarkupRule' => 'applications/spaces/remarkup/PhabricatorSpacesRemarkupRule.php',
+    'PhabricatorSpacesSchemaSpec' => 'applications/spaces/storage/PhabricatorSpacesSchemaSpec.php',
     'PhabricatorSpacesTestCase' => 'applications/spaces/__tests__/PhabricatorSpacesTestCase.php',
     'PhabricatorSpacesViewController' => 'applications/spaces/controller/PhabricatorSpacesViewController.php',
     'PhabricatorStandardCustomField' => 'infrastructure/customfield/standard/PhabricatorStandardCustomField.php',
     'PhabricatorStandardCustomFieldBool' => 'infrastructure/customfield/standard/PhabricatorStandardCustomFieldBool.php',
     'PhabricatorStandardCustomFieldCredential' => 'infrastructure/customfield/standard/PhabricatorStandardCustomFieldCredential.php',
     'PhabricatorStandardCustomFieldDate' => 'infrastructure/customfield/standard/PhabricatorStandardCustomFieldDate.php',
     'PhabricatorStandardCustomFieldHeader' => 'infrastructure/customfield/standard/PhabricatorStandardCustomFieldHeader.php',
     'PhabricatorStandardCustomFieldInt' => 'infrastructure/customfield/standard/PhabricatorStandardCustomFieldInt.php',
     'PhabricatorStandardCustomFieldInterface' => 'infrastructure/customfield/interface/PhabricatorStandardCustomFieldInterface.php',
     'PhabricatorStandardCustomFieldLink' => 'infrastructure/customfield/standard/PhabricatorStandardCustomFieldLink.php',
     'PhabricatorStandardCustomFieldPHIDs' => 'infrastructure/customfield/standard/PhabricatorStandardCustomFieldPHIDs.php',
     'PhabricatorStandardCustomFieldRemarkup' => 'infrastructure/customfield/standard/PhabricatorStandardCustomFieldRemarkup.php',
     'PhabricatorStandardCustomFieldSelect' => 'infrastructure/customfield/standard/PhabricatorStandardCustomFieldSelect.php',
     'PhabricatorStandardCustomFieldText' => 'infrastructure/customfield/standard/PhabricatorStandardCustomFieldText.php',
     'PhabricatorStandardCustomFieldUsers' => 'infrastructure/customfield/standard/PhabricatorStandardCustomFieldUsers.php',
     'PhabricatorStandardPageView' => 'view/page/PhabricatorStandardPageView.php',
     'PhabricatorStatusController' => 'applications/system/controller/PhabricatorStatusController.php',
     'PhabricatorStatusUIExample' => 'applications/uiexample/examples/PhabricatorStatusUIExample.php',
     'PhabricatorStorageFixtureScopeGuard' => 'infrastructure/testing/fixture/PhabricatorStorageFixtureScopeGuard.php',
     'PhabricatorStorageManagementAPI' => 'infrastructure/storage/management/PhabricatorStorageManagementAPI.php',
     'PhabricatorStorageManagementAdjustWorkflow' => 'infrastructure/storage/management/workflow/PhabricatorStorageManagementAdjustWorkflow.php',
     'PhabricatorStorageManagementDatabasesWorkflow' => 'infrastructure/storage/management/workflow/PhabricatorStorageManagementDatabasesWorkflow.php',
     'PhabricatorStorageManagementDestroyWorkflow' => 'infrastructure/storage/management/workflow/PhabricatorStorageManagementDestroyWorkflow.php',
     'PhabricatorStorageManagementDumpWorkflow' => 'infrastructure/storage/management/workflow/PhabricatorStorageManagementDumpWorkflow.php',
     'PhabricatorStorageManagementProbeWorkflow' => 'infrastructure/storage/management/workflow/PhabricatorStorageManagementProbeWorkflow.php',
     'PhabricatorStorageManagementQuickstartWorkflow' => 'infrastructure/storage/management/workflow/PhabricatorStorageManagementQuickstartWorkflow.php',
     'PhabricatorStorageManagementRenamespaceWorkflow' => 'infrastructure/storage/management/workflow/PhabricatorStorageManagementRenamespaceWorkflow.php',
     'PhabricatorStorageManagementShellWorkflow' => 'infrastructure/storage/management/workflow/PhabricatorStorageManagementShellWorkflow.php',
     'PhabricatorStorageManagementStatusWorkflow' => 'infrastructure/storage/management/workflow/PhabricatorStorageManagementStatusWorkflow.php',
     'PhabricatorStorageManagementUpgradeWorkflow' => 'infrastructure/storage/management/workflow/PhabricatorStorageManagementUpgradeWorkflow.php',
     'PhabricatorStorageManagementWorkflow' => 'infrastructure/storage/management/workflow/PhabricatorStorageManagementWorkflow.php',
     'PhabricatorStoragePatch' => 'infrastructure/storage/management/PhabricatorStoragePatch.php',
     'PhabricatorStorageSchemaSpec' => 'infrastructure/storage/schema/PhabricatorStorageSchemaSpec.php',
     'PhabricatorStorageSetupCheck' => 'applications/config/check/PhabricatorStorageSetupCheck.php',
     'PhabricatorStreamingProtocolAdapter' => 'infrastructure/daemon/bot/adapter/PhabricatorStreamingProtocolAdapter.php',
     'PhabricatorSubscribableInterface' => 'applications/subscriptions/interface/PhabricatorSubscribableInterface.php',
     'PhabricatorSubscribedToObjectEdgeType' => 'applications/transactions/edges/PhabricatorSubscribedToObjectEdgeType.php',
     'PhabricatorSubscribersQuery' => 'applications/subscriptions/query/PhabricatorSubscribersQuery.php',
     'PhabricatorSubscriptionTriggerClock' => 'infrastructure/daemon/workers/clock/PhabricatorSubscriptionTriggerClock.php',
     'PhabricatorSubscriptionsApplication' => 'applications/subscriptions/application/PhabricatorSubscriptionsApplication.php',
     'PhabricatorSubscriptionsEditController' => 'applications/subscriptions/controller/PhabricatorSubscriptionsEditController.php',
     'PhabricatorSubscriptionsEditor' => 'applications/subscriptions/editor/PhabricatorSubscriptionsEditor.php',
     'PhabricatorSubscriptionsListController' => 'applications/subscriptions/controller/PhabricatorSubscriptionsListController.php',
     'PhabricatorSubscriptionsSubscribeEmailCommand' => 'applications/subscriptions/command/PhabricatorSubscriptionsSubscribeEmailCommand.php',
+    'PhabricatorSubscriptionsSubscribersPolicyRule' => 'applications/subscriptions/policyrule/PhabricatorSubscriptionsSubscribersPolicyRule.php',
     'PhabricatorSubscriptionsTransactionController' => 'applications/subscriptions/controller/PhabricatorSubscriptionsTransactionController.php',
     'PhabricatorSubscriptionsUIEventListener' => 'applications/subscriptions/events/PhabricatorSubscriptionsUIEventListener.php',
     'PhabricatorSubscriptionsUnsubscribeEmailCommand' => 'applications/subscriptions/command/PhabricatorSubscriptionsUnsubscribeEmailCommand.php',
     'PhabricatorSupportApplication' => 'applications/support/application/PhabricatorSupportApplication.php',
     'PhabricatorSyntaxHighlighter' => 'infrastructure/markup/PhabricatorSyntaxHighlighter.php',
     'PhabricatorSyntaxHighlightingConfigOptions' => 'applications/config/option/PhabricatorSyntaxHighlightingConfigOptions.php',
     'PhabricatorSystemAction' => 'applications/system/action/PhabricatorSystemAction.php',
     'PhabricatorSystemActionEngine' => 'applications/system/engine/PhabricatorSystemActionEngine.php',
     'PhabricatorSystemActionGarbageCollector' => 'applications/system/garbagecollector/PhabricatorSystemActionGarbageCollector.php',
     'PhabricatorSystemActionLog' => 'applications/system/storage/PhabricatorSystemActionLog.php',
     'PhabricatorSystemActionRateLimitException' => 'applications/system/exception/PhabricatorSystemActionRateLimitException.php',
     'PhabricatorSystemApplication' => 'applications/system/application/PhabricatorSystemApplication.php',
     'PhabricatorSystemDAO' => 'applications/system/storage/PhabricatorSystemDAO.php',
     'PhabricatorSystemDestructionGarbageCollector' => 'applications/system/garbagecollector/PhabricatorSystemDestructionGarbageCollector.php',
     'PhabricatorSystemDestructionLog' => 'applications/system/storage/PhabricatorSystemDestructionLog.php',
     'PhabricatorSystemRemoveDestroyWorkflow' => 'applications/system/management/PhabricatorSystemRemoveDestroyWorkflow.php',
     'PhabricatorSystemRemoveLogWorkflow' => 'applications/system/management/PhabricatorSystemRemoveLogWorkflow.php',
     'PhabricatorSystemRemoveWorkflow' => 'applications/system/management/PhabricatorSystemRemoveWorkflow.php',
     'PhabricatorSystemSelectEncodingController' => 'applications/system/controller/PhabricatorSystemSelectEncodingController.php',
     'PhabricatorSystemSelectHighlightController' => 'applications/system/controller/PhabricatorSystemSelectHighlightController.php',
     'PhabricatorTOTPAuthFactor' => 'applications/auth/factor/PhabricatorTOTPAuthFactor.php',
     'PhabricatorTOTPAuthFactorTestCase' => 'applications/auth/factor/__tests__/PhabricatorTOTPAuthFactorTestCase.php',
     'PhabricatorTaskmasterDaemon' => 'infrastructure/daemon/workers/PhabricatorTaskmasterDaemon.php',
     'PhabricatorTestApplication' => 'applications/base/controller/__tests__/PhabricatorTestApplication.php',
     'PhabricatorTestCase' => 'infrastructure/testing/PhabricatorTestCase.php',
     'PhabricatorTestController' => 'applications/base/controller/__tests__/PhabricatorTestController.php',
     'PhabricatorTestDataGenerator' => 'applications/lipsum/generator/PhabricatorTestDataGenerator.php',
     'PhabricatorTestNoCycleEdgeType' => 'applications/transactions/edges/PhabricatorTestNoCycleEdgeType.php',
     'PhabricatorTestStorageEngine' => 'applications/files/engine/PhabricatorTestStorageEngine.php',
     'PhabricatorTestWorker' => 'infrastructure/daemon/workers/__tests__/PhabricatorTestWorker.php',
     'PhabricatorTime' => 'infrastructure/time/PhabricatorTime.php',
     'PhabricatorTimeGuard' => 'infrastructure/time/PhabricatorTimeGuard.php',
     'PhabricatorTimeTestCase' => 'infrastructure/time/__tests__/PhabricatorTimeTestCase.php',
     'PhabricatorTimezoneSetupCheck' => 'applications/config/check/PhabricatorTimezoneSetupCheck.php',
     'PhabricatorToken' => 'applications/tokens/storage/PhabricatorToken.php',
     'PhabricatorTokenController' => 'applications/tokens/controller/PhabricatorTokenController.php',
     'PhabricatorTokenCount' => 'applications/tokens/storage/PhabricatorTokenCount.php',
     'PhabricatorTokenCountQuery' => 'applications/tokens/query/PhabricatorTokenCountQuery.php',
     'PhabricatorTokenDAO' => 'applications/tokens/storage/PhabricatorTokenDAO.php',
     'PhabricatorTokenGiveController' => 'applications/tokens/controller/PhabricatorTokenGiveController.php',
     'PhabricatorTokenGiven' => 'applications/tokens/storage/PhabricatorTokenGiven.php',
     'PhabricatorTokenGivenController' => 'applications/tokens/controller/PhabricatorTokenGivenController.php',
     'PhabricatorTokenGivenEditor' => 'applications/tokens/editor/PhabricatorTokenGivenEditor.php',
     'PhabricatorTokenGivenFeedStory' => 'applications/tokens/feed/PhabricatorTokenGivenFeedStory.php',
     'PhabricatorTokenGivenQuery' => 'applications/tokens/query/PhabricatorTokenGivenQuery.php',
     'PhabricatorTokenLeaderController' => 'applications/tokens/controller/PhabricatorTokenLeaderController.php',
     'PhabricatorTokenQuery' => 'applications/tokens/query/PhabricatorTokenQuery.php',
     'PhabricatorTokenReceiverInterface' => 'applications/tokens/interface/PhabricatorTokenReceiverInterface.php',
     'PhabricatorTokenReceiverQuery' => 'applications/tokens/query/PhabricatorTokenReceiverQuery.php',
     'PhabricatorTokenTokenPHIDType' => 'applications/tokens/phid/PhabricatorTokenTokenPHIDType.php',
     'PhabricatorTokenUIEventListener' => 'applications/tokens/event/PhabricatorTokenUIEventListener.php',
     'PhabricatorTokensApplication' => 'applications/tokens/application/PhabricatorTokensApplication.php',
     'PhabricatorTokensSettingsPanel' => 'applications/settings/panel/PhabricatorTokensSettingsPanel.php',
     'PhabricatorTooltipUIExample' => 'applications/uiexample/examples/PhabricatorTooltipUIExample.php',
     'PhabricatorTransactions' => 'applications/transactions/constants/PhabricatorTransactions.php',
     'PhabricatorTransactionsApplication' => 'applications/transactions/application/PhabricatorTransactionsApplication.php',
     'PhabricatorTransformedFile' => 'applications/files/storage/PhabricatorTransformedFile.php',
     'PhabricatorTranslationsConfigOptions' => 'applications/config/option/PhabricatorTranslationsConfigOptions.php',
     'PhabricatorTriggerAction' => 'infrastructure/daemon/workers/action/PhabricatorTriggerAction.php',
     'PhabricatorTriggerClock' => 'infrastructure/daemon/workers/clock/PhabricatorTriggerClock.php',
     'PhabricatorTriggerClockTestCase' => 'infrastructure/daemon/workers/clock/__tests__/PhabricatorTriggerClockTestCase.php',
     'PhabricatorTriggerDaemon' => 'infrastructure/daemon/workers/PhabricatorTriggerDaemon.php',
     'PhabricatorTrivialTestCase' => 'infrastructure/testing/__tests__/PhabricatorTrivialTestCase.php',
     'PhabricatorTwitchAuthProvider' => 'applications/auth/provider/PhabricatorTwitchAuthProvider.php',
     'PhabricatorTwitterAuthProvider' => 'applications/auth/provider/PhabricatorTwitterAuthProvider.php',
     'PhabricatorTwoColumnUIExample' => 'applications/uiexample/examples/PhabricatorTwoColumnUIExample.php',
     'PhabricatorTypeaheadApplication' => 'applications/typeahead/application/PhabricatorTypeaheadApplication.php',
     'PhabricatorTypeaheadCompositeDatasource' => 'applications/typeahead/datasource/PhabricatorTypeaheadCompositeDatasource.php',
     'PhabricatorTypeaheadDatasource' => 'applications/typeahead/datasource/PhabricatorTypeaheadDatasource.php',
     'PhabricatorTypeaheadDatasourceController' => 'applications/typeahead/controller/PhabricatorTypeaheadDatasourceController.php',
     'PhabricatorTypeaheadFunctionHelpController' => 'applications/typeahead/controller/PhabricatorTypeaheadFunctionHelpController.php',
     'PhabricatorTypeaheadInvalidTokenException' => 'applications/typeahead/exception/PhabricatorTypeaheadInvalidTokenException.php',
     'PhabricatorTypeaheadModularDatasourceController' => 'applications/typeahead/controller/PhabricatorTypeaheadModularDatasourceController.php',
     'PhabricatorTypeaheadMonogramDatasource' => 'applications/typeahead/datasource/PhabricatorTypeaheadMonogramDatasource.php',
     'PhabricatorTypeaheadResult' => 'applications/typeahead/storage/PhabricatorTypeaheadResult.php',
     'PhabricatorTypeaheadRuntimeCompositeDatasource' => 'applications/typeahead/datasource/PhabricatorTypeaheadRuntimeCompositeDatasource.php',
     'PhabricatorTypeaheadTokenView' => 'applications/typeahead/view/PhabricatorTypeaheadTokenView.php',
     'PhabricatorUIConfigOptions' => 'applications/config/option/PhabricatorUIConfigOptions.php',
     'PhabricatorUIExample' => 'applications/uiexample/examples/PhabricatorUIExample.php',
     'PhabricatorUIExampleRenderController' => 'applications/uiexample/controller/PhabricatorUIExampleRenderController.php',
     'PhabricatorUIExamplesApplication' => 'applications/uiexample/application/PhabricatorUIExamplesApplication.php',
     'PhabricatorUSEnglishTranslation' => 'infrastructure/internationalization/translation/PhabricatorUSEnglishTranslation.php',
     'PhabricatorUnitsTestCase' => 'view/__tests__/PhabricatorUnitsTestCase.php',
     'PhabricatorUnsubscribedFromObjectEdgeType' => 'applications/transactions/edges/PhabricatorUnsubscribedFromObjectEdgeType.php',
     'PhabricatorUser' => 'applications/people/storage/PhabricatorUser.php',
     'PhabricatorUserBlurbField' => 'applications/people/customfield/PhabricatorUserBlurbField.php',
     'PhabricatorUserConfigOptions' => 'applications/people/config/PhabricatorUserConfigOptions.php',
     'PhabricatorUserConfiguredCustomField' => 'applications/people/customfield/PhabricatorUserConfiguredCustomField.php',
     'PhabricatorUserConfiguredCustomFieldStorage' => 'applications/people/storage/PhabricatorUserConfiguredCustomFieldStorage.php',
     'PhabricatorUserCustomField' => 'applications/people/customfield/PhabricatorUserCustomField.php',
     'PhabricatorUserCustomFieldNumericIndex' => 'applications/people/storage/PhabricatorUserCustomFieldNumericIndex.php',
     'PhabricatorUserCustomFieldStringIndex' => 'applications/people/storage/PhabricatorUserCustomFieldStringIndex.php',
     'PhabricatorUserDAO' => 'applications/people/storage/PhabricatorUserDAO.php',
     'PhabricatorUserEditor' => 'applications/people/editor/PhabricatorUserEditor.php',
     'PhabricatorUserEditorTestCase' => 'applications/people/editor/__tests__/PhabricatorUserEditorTestCase.php',
     'PhabricatorUserEmail' => 'applications/people/storage/PhabricatorUserEmail.php',
     'PhabricatorUserEmailTestCase' => 'applications/people/storage/__tests__/PhabricatorUserEmailTestCase.php',
     'PhabricatorUserLog' => 'applications/people/storage/PhabricatorUserLog.php',
     'PhabricatorUserLogView' => 'applications/people/view/PhabricatorUserLogView.php',
     'PhabricatorUserPreferences' => 'applications/settings/storage/PhabricatorUserPreferences.php',
     'PhabricatorUserProfile' => 'applications/people/storage/PhabricatorUserProfile.php',
     'PhabricatorUserProfileEditor' => 'applications/people/editor/PhabricatorUserProfileEditor.php',
     'PhabricatorUserRealNameField' => 'applications/people/customfield/PhabricatorUserRealNameField.php',
     'PhabricatorUserRolesField' => 'applications/people/customfield/PhabricatorUserRolesField.php',
     'PhabricatorUserSchemaSpec' => 'applications/people/storage/PhabricatorUserSchemaSpec.php',
     'PhabricatorUserSearchIndexer' => 'applications/people/search/PhabricatorUserSearchIndexer.php',
     'PhabricatorUserSinceField' => 'applications/people/customfield/PhabricatorUserSinceField.php',
     'PhabricatorUserStatusField' => 'applications/people/customfield/PhabricatorUserStatusField.php',
     'PhabricatorUserTestCase' => 'applications/people/storage/__tests__/PhabricatorUserTestCase.php',
     'PhabricatorUserTitleField' => 'applications/people/customfield/PhabricatorUserTitleField.php',
     'PhabricatorUserTransaction' => 'applications/people/storage/PhabricatorUserTransaction.php',
     'PhabricatorUsersPolicyRule' => 'applications/policy/rule/PhabricatorUsersPolicyRule.php',
     'PhabricatorVCSResponse' => 'applications/repository/response/PhabricatorVCSResponse.php',
     'PhabricatorVeryWowEnglishTranslation' => 'infrastructure/internationalization/translation/PhabricatorVeryWowEnglishTranslation.php',
     'PhabricatorViewerDatasource' => 'applications/people/typeahead/PhabricatorViewerDatasource.php',
     'PhabricatorWatcherHasObjectEdgeType' => 'applications/transactions/edges/PhabricatorWatcherHasObjectEdgeType.php',
     'PhabricatorWordPressAuthProvider' => 'applications/auth/provider/PhabricatorWordPressAuthProvider.php',
     'PhabricatorWorker' => 'infrastructure/daemon/workers/PhabricatorWorker.php',
     'PhabricatorWorkerActiveTask' => 'infrastructure/daemon/workers/storage/PhabricatorWorkerActiveTask.php',
     'PhabricatorWorkerArchiveTask' => 'infrastructure/daemon/workers/storage/PhabricatorWorkerArchiveTask.php',
     'PhabricatorWorkerArchiveTaskQuery' => 'infrastructure/daemon/workers/query/PhabricatorWorkerArchiveTaskQuery.php',
     'PhabricatorWorkerDAO' => 'infrastructure/daemon/workers/storage/PhabricatorWorkerDAO.php',
     'PhabricatorWorkerLeaseQuery' => 'infrastructure/daemon/workers/query/PhabricatorWorkerLeaseQuery.php',
     'PhabricatorWorkerManagementCancelWorkflow' => 'infrastructure/daemon/workers/management/PhabricatorWorkerManagementCancelWorkflow.php',
     'PhabricatorWorkerManagementExecuteWorkflow' => 'infrastructure/daemon/workers/management/PhabricatorWorkerManagementExecuteWorkflow.php',
     'PhabricatorWorkerManagementFloodWorkflow' => 'infrastructure/daemon/workers/management/PhabricatorWorkerManagementFloodWorkflow.php',
     'PhabricatorWorkerManagementFreeWorkflow' => 'infrastructure/daemon/workers/management/PhabricatorWorkerManagementFreeWorkflow.php',
     'PhabricatorWorkerManagementRetryWorkflow' => 'infrastructure/daemon/workers/management/PhabricatorWorkerManagementRetryWorkflow.php',
     'PhabricatorWorkerManagementWorkflow' => 'infrastructure/daemon/workers/management/PhabricatorWorkerManagementWorkflow.php',
     'PhabricatorWorkerPermanentFailureException' => 'infrastructure/daemon/workers/exception/PhabricatorWorkerPermanentFailureException.php',
     'PhabricatorWorkerTask' => 'infrastructure/daemon/workers/storage/PhabricatorWorkerTask.php',
     'PhabricatorWorkerTaskData' => 'infrastructure/daemon/workers/storage/PhabricatorWorkerTaskData.php',
     'PhabricatorWorkerTaskDetailController' => 'applications/daemon/controller/PhabricatorWorkerTaskDetailController.php',
     'PhabricatorWorkerTestCase' => 'infrastructure/daemon/workers/__tests__/PhabricatorWorkerTestCase.php',
     'PhabricatorWorkerTrigger' => 'infrastructure/daemon/workers/storage/PhabricatorWorkerTrigger.php',
     'PhabricatorWorkerTriggerEvent' => 'infrastructure/daemon/workers/storage/PhabricatorWorkerTriggerEvent.php',
     'PhabricatorWorkerTriggerManagementFireWorkflow' => 'infrastructure/daemon/workers/management/PhabricatorWorkerTriggerManagementFireWorkflow.php',
     'PhabricatorWorkerTriggerManagementWorkflow' => 'infrastructure/daemon/workers/management/PhabricatorWorkerTriggerManagementWorkflow.php',
     'PhabricatorWorkerTriggerPHIDType' => 'infrastructure/daemon/workers/phid/PhabricatorWorkerTriggerPHIDType.php',
     'PhabricatorWorkerTriggerQuery' => 'infrastructure/daemon/workers/query/PhabricatorWorkerTriggerQuery.php',
     'PhabricatorWorkerYieldException' => 'infrastructure/daemon/workers/exception/PhabricatorWorkerYieldException.php',
     'PhabricatorWorkingCopyDiscoveryTestCase' => 'applications/repository/engine/__tests__/PhabricatorWorkingCopyDiscoveryTestCase.php',
     'PhabricatorWorkingCopyPullTestCase' => 'applications/repository/engine/__tests__/PhabricatorWorkingCopyPullTestCase.php',
     'PhabricatorWorkingCopyTestCase' => 'applications/repository/engine/__tests__/PhabricatorWorkingCopyTestCase.php',
     'PhabricatorXHPASTViewController' => 'applications/phpast/controller/PhabricatorXHPASTViewController.php',
     'PhabricatorXHPASTViewDAO' => 'applications/phpast/storage/PhabricatorXHPASTViewDAO.php',
     'PhabricatorXHPASTViewFrameController' => 'applications/phpast/controller/PhabricatorXHPASTViewFrameController.php',
     'PhabricatorXHPASTViewFramesetController' => 'applications/phpast/controller/PhabricatorXHPASTViewFramesetController.php',
     'PhabricatorXHPASTViewInputController' => 'applications/phpast/controller/PhabricatorXHPASTViewInputController.php',
     'PhabricatorXHPASTViewPanelController' => 'applications/phpast/controller/PhabricatorXHPASTViewPanelController.php',
     'PhabricatorXHPASTViewParseTree' => 'applications/phpast/storage/PhabricatorXHPASTViewParseTree.php',
     'PhabricatorXHPASTViewRunController' => 'applications/phpast/controller/PhabricatorXHPASTViewRunController.php',
     'PhabricatorXHPASTViewStreamController' => 'applications/phpast/controller/PhabricatorXHPASTViewStreamController.php',
     'PhabricatorXHPASTViewTreeController' => 'applications/phpast/controller/PhabricatorXHPASTViewTreeController.php',
     'PhabricatorXHProfApplication' => 'applications/xhprof/application/PhabricatorXHProfApplication.php',
     'PhabricatorXHProfController' => 'applications/xhprof/controller/PhabricatorXHProfController.php',
     'PhabricatorXHProfDAO' => 'applications/xhprof/storage/PhabricatorXHProfDAO.php',
     'PhabricatorXHProfProfileController' => 'applications/xhprof/controller/PhabricatorXHProfProfileController.php',
     'PhabricatorXHProfProfileSymbolView' => 'applications/xhprof/view/PhabricatorXHProfProfileSymbolView.php',
     'PhabricatorXHProfProfileTopLevelView' => 'applications/xhprof/view/PhabricatorXHProfProfileTopLevelView.php',
     'PhabricatorXHProfProfileView' => 'applications/xhprof/view/PhabricatorXHProfProfileView.php',
     'PhabricatorXHProfSample' => 'applications/xhprof/storage/PhabricatorXHProfSample.php',
     'PhabricatorXHProfSampleListController' => 'applications/xhprof/controller/PhabricatorXHProfSampleListController.php',
     'PhabricatorYoutubeRemarkupRule' => 'infrastructure/markup/rule/PhabricatorYoutubeRemarkupRule.php',
     'PhameBasicBlogSkin' => 'applications/phame/skins/PhameBasicBlogSkin.php',
     'PhameBasicTemplateBlogSkin' => 'applications/phame/skins/PhameBasicTemplateBlogSkin.php',
     'PhameBlog' => 'applications/phame/storage/PhameBlog.php',
     'PhameBlogDeleteController' => 'applications/phame/controller/blog/PhameBlogDeleteController.php',
     'PhameBlogEditController' => 'applications/phame/controller/blog/PhameBlogEditController.php',
     'PhameBlogEditor' => 'applications/phame/editor/PhameBlogEditor.php',
     'PhameBlogFeedController' => 'applications/phame/controller/blog/PhameBlogFeedController.php',
     'PhameBlogListController' => 'applications/phame/controller/blog/PhameBlogListController.php',
     'PhameBlogLiveController' => 'applications/phame/controller/blog/PhameBlogLiveController.php',
     'PhameBlogQuery' => 'applications/phame/query/PhameBlogQuery.php',
     'PhameBlogSkin' => 'applications/phame/skins/PhameBlogSkin.php',
     'PhameBlogTransaction' => 'applications/phame/storage/PhameBlogTransaction.php',
     'PhameBlogViewController' => 'applications/phame/controller/blog/PhameBlogViewController.php',
     'PhameCelerityResources' => 'applications/phame/celerity/PhameCelerityResources.php',
     'PhameConduitAPIMethod' => 'applications/phame/conduit/PhameConduitAPIMethod.php',
     'PhameController' => 'applications/phame/controller/PhameController.php',
     'PhameCreatePostConduitAPIMethod' => 'applications/phame/conduit/PhameCreatePostConduitAPIMethod.php',
     'PhameDAO' => 'applications/phame/storage/PhameDAO.php',
     'PhamePost' => 'applications/phame/storage/PhamePost.php',
     'PhamePostDeleteController' => 'applications/phame/controller/post/PhamePostDeleteController.php',
     'PhamePostEditController' => 'applications/phame/controller/post/PhamePostEditController.php',
     'PhamePostEditor' => 'applications/phame/editor/PhamePostEditor.php',
     'PhamePostFramedController' => 'applications/phame/controller/post/PhamePostFramedController.php',
     'PhamePostListController' => 'applications/phame/controller/post/PhamePostListController.php',
     'PhamePostNewController' => 'applications/phame/controller/post/PhamePostNewController.php',
     'PhamePostNotLiveController' => 'applications/phame/controller/post/PhamePostNotLiveController.php',
     'PhamePostPreviewController' => 'applications/phame/controller/post/PhamePostPreviewController.php',
     'PhamePostPublishController' => 'applications/phame/controller/post/PhamePostPublishController.php',
     'PhamePostQuery' => 'applications/phame/query/PhamePostQuery.php',
     'PhamePostTransaction' => 'applications/phame/storage/PhamePostTransaction.php',
     'PhamePostTransactionQuery' => 'applications/phame/query/PhamePostTransactionQuery.php',
     'PhamePostUnpublishController' => 'applications/phame/controller/post/PhamePostUnpublishController.php',
     'PhamePostView' => 'applications/phame/view/PhamePostView.php',
     'PhamePostViewController' => 'applications/phame/controller/post/PhamePostViewController.php',
     'PhameQueryConduitAPIMethod' => 'applications/phame/conduit/PhameQueryConduitAPIMethod.php',
     'PhameQueryPostsConduitAPIMethod' => 'applications/phame/conduit/PhameQueryPostsConduitAPIMethod.php',
     'PhameResourceController' => 'applications/phame/controller/PhameResourceController.php',
     'PhameSchemaSpec' => 'applications/phame/storage/PhameSchemaSpec.php',
     'PhameSkinSpecification' => 'applications/phame/skins/PhameSkinSpecification.php',
     'PhluxController' => 'applications/phlux/controller/PhluxController.php',
     'PhluxDAO' => 'applications/phlux/storage/PhluxDAO.php',
     'PhluxEditController' => 'applications/phlux/controller/PhluxEditController.php',
     'PhluxListController' => 'applications/phlux/controller/PhluxListController.php',
     'PhluxTransaction' => 'applications/phlux/storage/PhluxTransaction.php',
     'PhluxTransactionQuery' => 'applications/phlux/query/PhluxTransactionQuery.php',
     'PhluxVariable' => 'applications/phlux/storage/PhluxVariable.php',
     'PhluxVariableEditor' => 'applications/phlux/editor/PhluxVariableEditor.php',
     'PhluxVariablePHIDType' => 'applications/phlux/phid/PhluxVariablePHIDType.php',
     'PhluxVariableQuery' => 'applications/phlux/query/PhluxVariableQuery.php',
     'PhluxViewController' => 'applications/phlux/controller/PhluxViewController.php',
     'PholioActionMenuEventListener' => 'applications/pholio/event/PholioActionMenuEventListener.php',
     'PholioController' => 'applications/pholio/controller/PholioController.php',
     'PholioDAO' => 'applications/pholio/storage/PholioDAO.php',
     'PholioDefaultEditCapability' => 'applications/pholio/capability/PholioDefaultEditCapability.php',
     'PholioDefaultViewCapability' => 'applications/pholio/capability/PholioDefaultViewCapability.php',
     'PholioImage' => 'applications/pholio/storage/PholioImage.php',
     'PholioImagePHIDType' => 'applications/pholio/phid/PholioImagePHIDType.php',
     'PholioImageQuery' => 'applications/pholio/query/PholioImageQuery.php',
     'PholioImageUploadController' => 'applications/pholio/controller/PholioImageUploadController.php',
     'PholioInlineController' => 'applications/pholio/controller/PholioInlineController.php',
     'PholioInlineListController' => 'applications/pholio/controller/PholioInlineListController.php',
     'PholioMock' => 'applications/pholio/storage/PholioMock.php',
     'PholioMockCommentController' => 'applications/pholio/controller/PholioMockCommentController.php',
     'PholioMockEditController' => 'applications/pholio/controller/PholioMockEditController.php',
     'PholioMockEditor' => 'applications/pholio/editor/PholioMockEditor.php',
     'PholioMockEmbedView' => 'applications/pholio/view/PholioMockEmbedView.php',
     'PholioMockHasTaskEdgeType' => 'applications/pholio/edge/PholioMockHasTaskEdgeType.php',
     'PholioMockImagesView' => 'applications/pholio/view/PholioMockImagesView.php',
     'PholioMockListController' => 'applications/pholio/controller/PholioMockListController.php',
     'PholioMockMailReceiver' => 'applications/pholio/mail/PholioMockMailReceiver.php',
     'PholioMockPHIDType' => 'applications/pholio/phid/PholioMockPHIDType.php',
     'PholioMockQuery' => 'applications/pholio/query/PholioMockQuery.php',
     'PholioMockSearchEngine' => 'applications/pholio/query/PholioMockSearchEngine.php',
     'PholioMockThumbGridView' => 'applications/pholio/view/PholioMockThumbGridView.php',
     'PholioMockViewController' => 'applications/pholio/controller/PholioMockViewController.php',
     'PholioRemarkupRule' => 'applications/pholio/remarkup/PholioRemarkupRule.php',
     'PholioReplyHandler' => 'applications/pholio/mail/PholioReplyHandler.php',
     'PholioSchemaSpec' => 'applications/pholio/storage/PholioSchemaSpec.php',
     'PholioSearchIndexer' => 'applications/pholio/search/PholioSearchIndexer.php',
     'PholioTransaction' => 'applications/pholio/storage/PholioTransaction.php',
     'PholioTransactionComment' => 'applications/pholio/storage/PholioTransactionComment.php',
     'PholioTransactionQuery' => 'applications/pholio/query/PholioTransactionQuery.php',
     'PholioTransactionView' => 'applications/pholio/view/PholioTransactionView.php',
     'PholioUploadedImageView' => 'applications/pholio/view/PholioUploadedImageView.php',
     'PhortuneAccount' => 'applications/phortune/storage/PhortuneAccount.php',
     'PhortuneAccountEditController' => 'applications/phortune/controller/PhortuneAccountEditController.php',
     'PhortuneAccountEditor' => 'applications/phortune/editor/PhortuneAccountEditor.php',
     'PhortuneAccountHasMemberEdgeType' => 'applications/phortune/edge/PhortuneAccountHasMemberEdgeType.php',
     'PhortuneAccountListController' => 'applications/phortune/controller/PhortuneAccountListController.php',
     'PhortuneAccountPHIDType' => 'applications/phortune/phid/PhortuneAccountPHIDType.php',
     'PhortuneAccountQuery' => 'applications/phortune/query/PhortuneAccountQuery.php',
     'PhortuneAccountTransaction' => 'applications/phortune/storage/PhortuneAccountTransaction.php',
     'PhortuneAccountTransactionQuery' => 'applications/phortune/query/PhortuneAccountTransactionQuery.php',
     'PhortuneAccountViewController' => 'applications/phortune/controller/PhortuneAccountViewController.php',
     'PhortuneAdHocCart' => 'applications/phortune/cart/PhortuneAdHocCart.php',
     'PhortuneAdHocProduct' => 'applications/phortune/product/PhortuneAdHocProduct.php',
     'PhortuneCart' => 'applications/phortune/storage/PhortuneCart.php',
     'PhortuneCartAcceptController' => 'applications/phortune/controller/PhortuneCartAcceptController.php',
     'PhortuneCartCancelController' => 'applications/phortune/controller/PhortuneCartCancelController.php',
     'PhortuneCartCheckoutController' => 'applications/phortune/controller/PhortuneCartCheckoutController.php',
     'PhortuneCartController' => 'applications/phortune/controller/PhortuneCartController.php',
     'PhortuneCartEditor' => 'applications/phortune/editor/PhortuneCartEditor.php',
     'PhortuneCartImplementation' => 'applications/phortune/cart/PhortuneCartImplementation.php',
     'PhortuneCartListController' => 'applications/phortune/controller/PhortuneCartListController.php',
     'PhortuneCartPHIDType' => 'applications/phortune/phid/PhortuneCartPHIDType.php',
     'PhortuneCartQuery' => 'applications/phortune/query/PhortuneCartQuery.php',
     'PhortuneCartReplyHandler' => 'applications/phortune/mail/PhortuneCartReplyHandler.php',
     'PhortuneCartSearchEngine' => 'applications/phortune/query/PhortuneCartSearchEngine.php',
     'PhortuneCartTransaction' => 'applications/phortune/storage/PhortuneCartTransaction.php',
     'PhortuneCartTransactionQuery' => 'applications/phortune/query/PhortuneCartTransactionQuery.php',
     'PhortuneCartUpdateController' => 'applications/phortune/controller/PhortuneCartUpdateController.php',
     'PhortuneCartViewController' => 'applications/phortune/controller/PhortuneCartViewController.php',
     'PhortuneCharge' => 'applications/phortune/storage/PhortuneCharge.php',
     'PhortuneChargeListController' => 'applications/phortune/controller/PhortuneChargeListController.php',
     'PhortuneChargePHIDType' => 'applications/phortune/phid/PhortuneChargePHIDType.php',
     'PhortuneChargeQuery' => 'applications/phortune/query/PhortuneChargeQuery.php',
     'PhortuneChargeSearchEngine' => 'applications/phortune/query/PhortuneChargeSearchEngine.php',
     'PhortuneChargeTableView' => 'applications/phortune/view/PhortuneChargeTableView.php',
     'PhortuneConstants' => 'applications/phortune/constants/PhortuneConstants.php',
     'PhortuneController' => 'applications/phortune/controller/PhortuneController.php',
     'PhortuneCreditCardForm' => 'applications/phortune/view/PhortuneCreditCardForm.php',
     'PhortuneCurrency' => 'applications/phortune/currency/PhortuneCurrency.php',
     'PhortuneCurrencySerializer' => 'applications/phortune/currency/PhortuneCurrencySerializer.php',
     'PhortuneCurrencyTestCase' => 'applications/phortune/currency/__tests__/PhortuneCurrencyTestCase.php',
     'PhortuneDAO' => 'applications/phortune/storage/PhortuneDAO.php',
     'PhortuneErrCode' => 'applications/phortune/constants/PhortuneErrCode.php',
     'PhortuneLandingController' => 'applications/phortune/controller/PhortuneLandingController.php',
     'PhortuneMemberHasAccountEdgeType' => 'applications/phortune/edge/PhortuneMemberHasAccountEdgeType.php',
     'PhortuneMemberHasMerchantEdgeType' => 'applications/phortune/edge/PhortuneMemberHasMerchantEdgeType.php',
     'PhortuneMerchant' => 'applications/phortune/storage/PhortuneMerchant.php',
     'PhortuneMerchantCapability' => 'applications/phortune/capability/PhortuneMerchantCapability.php',
     'PhortuneMerchantController' => 'applications/phortune/controller/PhortuneMerchantController.php',
     'PhortuneMerchantEditController' => 'applications/phortune/controller/PhortuneMerchantEditController.php',
     'PhortuneMerchantEditor' => 'applications/phortune/editor/PhortuneMerchantEditor.php',
     'PhortuneMerchantHasMemberEdgeType' => 'applications/phortune/edge/PhortuneMerchantHasMemberEdgeType.php',
     'PhortuneMerchantInvoiceCreateController' => 'applications/phortune/controller/PhortuneMerchantInvoiceCreateController.php',
     'PhortuneMerchantListController' => 'applications/phortune/controller/PhortuneMerchantListController.php',
     'PhortuneMerchantPHIDType' => 'applications/phortune/phid/PhortuneMerchantPHIDType.php',
     'PhortuneMerchantQuery' => 'applications/phortune/query/PhortuneMerchantQuery.php',
     'PhortuneMerchantSearchEngine' => 'applications/phortune/query/PhortuneMerchantSearchEngine.php',
     'PhortuneMerchantTransaction' => 'applications/phortune/storage/PhortuneMerchantTransaction.php',
     'PhortuneMerchantTransactionQuery' => 'applications/phortune/query/PhortuneMerchantTransactionQuery.php',
     'PhortuneMerchantViewController' => 'applications/phortune/controller/PhortuneMerchantViewController.php',
     'PhortuneMonthYearExpiryControl' => 'applications/phortune/control/PhortuneMonthYearExpiryControl.php',
     'PhortuneNotImplementedException' => 'applications/phortune/exception/PhortuneNotImplementedException.php',
     'PhortuneOrderTableView' => 'applications/phortune/view/PhortuneOrderTableView.php',
     'PhortunePayPalPaymentProvider' => 'applications/phortune/provider/PhortunePayPalPaymentProvider.php',
     'PhortunePaymentMethod' => 'applications/phortune/storage/PhortunePaymentMethod.php',
     'PhortunePaymentMethodCreateController' => 'applications/phortune/controller/PhortunePaymentMethodCreateController.php',
     'PhortunePaymentMethodDisableController' => 'applications/phortune/controller/PhortunePaymentMethodDisableController.php',
     'PhortunePaymentMethodEditController' => 'applications/phortune/controller/PhortunePaymentMethodEditController.php',
     'PhortunePaymentMethodPHIDType' => 'applications/phortune/phid/PhortunePaymentMethodPHIDType.php',
     'PhortunePaymentMethodQuery' => 'applications/phortune/query/PhortunePaymentMethodQuery.php',
     'PhortunePaymentProvider' => 'applications/phortune/provider/PhortunePaymentProvider.php',
     'PhortunePaymentProviderConfig' => 'applications/phortune/storage/PhortunePaymentProviderConfig.php',
     'PhortunePaymentProviderConfigEditor' => 'applications/phortune/editor/PhortunePaymentProviderConfigEditor.php',
     'PhortunePaymentProviderConfigQuery' => 'applications/phortune/query/PhortunePaymentProviderConfigQuery.php',
     'PhortunePaymentProviderConfigTransaction' => 'applications/phortune/storage/PhortunePaymentProviderConfigTransaction.php',
     'PhortunePaymentProviderConfigTransactionQuery' => 'applications/phortune/query/PhortunePaymentProviderConfigTransactionQuery.php',
     'PhortunePaymentProviderPHIDType' => 'applications/phortune/phid/PhortunePaymentProviderPHIDType.php',
+    'PhortunePaymentProviderTestCase' => 'applications/phortune/provider/__tests__/PhortunePaymentProviderTestCase.php',
     'PhortuneProduct' => 'applications/phortune/storage/PhortuneProduct.php',
     'PhortuneProductImplementation' => 'applications/phortune/product/PhortuneProductImplementation.php',
     'PhortuneProductListController' => 'applications/phortune/controller/PhortuneProductListController.php',
     'PhortuneProductPHIDType' => 'applications/phortune/phid/PhortuneProductPHIDType.php',
     'PhortuneProductQuery' => 'applications/phortune/query/PhortuneProductQuery.php',
     'PhortuneProductViewController' => 'applications/phortune/controller/PhortuneProductViewController.php',
     'PhortuneProviderActionController' => 'applications/phortune/controller/PhortuneProviderActionController.php',
     'PhortuneProviderDisableController' => 'applications/phortune/controller/PhortuneProviderDisableController.php',
     'PhortuneProviderEditController' => 'applications/phortune/controller/PhortuneProviderEditController.php',
     'PhortunePurchase' => 'applications/phortune/storage/PhortunePurchase.php',
     'PhortunePurchasePHIDType' => 'applications/phortune/phid/PhortunePurchasePHIDType.php',
     'PhortunePurchaseQuery' => 'applications/phortune/query/PhortunePurchaseQuery.php',
     'PhortuneSchemaSpec' => 'applications/phortune/storage/PhortuneSchemaSpec.php',
     'PhortuneStripePaymentProvider' => 'applications/phortune/provider/PhortuneStripePaymentProvider.php',
     'PhortuneSubscription' => 'applications/phortune/storage/PhortuneSubscription.php',
     'PhortuneSubscriptionCart' => 'applications/phortune/cart/PhortuneSubscriptionCart.php',
     'PhortuneSubscriptionEditController' => 'applications/phortune/controller/PhortuneSubscriptionEditController.php',
     'PhortuneSubscriptionImplementation' => 'applications/phortune/subscription/PhortuneSubscriptionImplementation.php',
     'PhortuneSubscriptionListController' => 'applications/phortune/controller/PhortuneSubscriptionListController.php',
     'PhortuneSubscriptionPHIDType' => 'applications/phortune/phid/PhortuneSubscriptionPHIDType.php',
     'PhortuneSubscriptionProduct' => 'applications/phortune/product/PhortuneSubscriptionProduct.php',
     'PhortuneSubscriptionQuery' => 'applications/phortune/query/PhortuneSubscriptionQuery.php',
     'PhortuneSubscriptionSearchEngine' => 'applications/phortune/query/PhortuneSubscriptionSearchEngine.php',
     'PhortuneSubscriptionTableView' => 'applications/phortune/view/PhortuneSubscriptionTableView.php',
     'PhortuneSubscriptionViewController' => 'applications/phortune/controller/PhortuneSubscriptionViewController.php',
     'PhortuneSubscriptionWorker' => 'applications/phortune/worker/PhortuneSubscriptionWorker.php',
     'PhortuneTestPaymentProvider' => 'applications/phortune/provider/PhortuneTestPaymentProvider.php',
     'PhortuneWePayPaymentProvider' => 'applications/phortune/provider/PhortuneWePayPaymentProvider.php',
     'PhragmentBrowseController' => 'applications/phragment/controller/PhragmentBrowseController.php',
     'PhragmentCanCreateCapability' => 'applications/phragment/capability/PhragmentCanCreateCapability.php',
     'PhragmentConduitAPIMethod' => 'applications/phragment/conduit/PhragmentConduitAPIMethod.php',
     'PhragmentController' => 'applications/phragment/controller/PhragmentController.php',
     'PhragmentCreateController' => 'applications/phragment/controller/PhragmentCreateController.php',
     'PhragmentDAO' => 'applications/phragment/storage/PhragmentDAO.php',
     'PhragmentFragment' => 'applications/phragment/storage/PhragmentFragment.php',
     'PhragmentFragmentPHIDType' => 'applications/phragment/phid/PhragmentFragmentPHIDType.php',
     'PhragmentFragmentQuery' => 'applications/phragment/query/PhragmentFragmentQuery.php',
     'PhragmentFragmentVersion' => 'applications/phragment/storage/PhragmentFragmentVersion.php',
     'PhragmentFragmentVersionPHIDType' => 'applications/phragment/phid/PhragmentFragmentVersionPHIDType.php',
     'PhragmentFragmentVersionQuery' => 'applications/phragment/query/PhragmentFragmentVersionQuery.php',
     'PhragmentGetPatchConduitAPIMethod' => 'applications/phragment/conduit/PhragmentGetPatchConduitAPIMethod.php',
     'PhragmentHistoryController' => 'applications/phragment/controller/PhragmentHistoryController.php',
     'PhragmentPatchController' => 'applications/phragment/controller/PhragmentPatchController.php',
     'PhragmentPatchUtil' => 'applications/phragment/util/PhragmentPatchUtil.php',
     'PhragmentPolicyController' => 'applications/phragment/controller/PhragmentPolicyController.php',
     'PhragmentQueryFragmentsConduitAPIMethod' => 'applications/phragment/conduit/PhragmentQueryFragmentsConduitAPIMethod.php',
     'PhragmentRevertController' => 'applications/phragment/controller/PhragmentRevertController.php',
     'PhragmentSchemaSpec' => 'applications/phragment/storage/PhragmentSchemaSpec.php',
     'PhragmentSnapshot' => 'applications/phragment/storage/PhragmentSnapshot.php',
     'PhragmentSnapshotChild' => 'applications/phragment/storage/PhragmentSnapshotChild.php',
     'PhragmentSnapshotChildQuery' => 'applications/phragment/query/PhragmentSnapshotChildQuery.php',
     'PhragmentSnapshotCreateController' => 'applications/phragment/controller/PhragmentSnapshotCreateController.php',
     'PhragmentSnapshotDeleteController' => 'applications/phragment/controller/PhragmentSnapshotDeleteController.php',
     'PhragmentSnapshotPHIDType' => 'applications/phragment/phid/PhragmentSnapshotPHIDType.php',
     'PhragmentSnapshotPromoteController' => 'applications/phragment/controller/PhragmentSnapshotPromoteController.php',
     'PhragmentSnapshotQuery' => 'applications/phragment/query/PhragmentSnapshotQuery.php',
     'PhragmentSnapshotViewController' => 'applications/phragment/controller/PhragmentSnapshotViewController.php',
     'PhragmentUpdateController' => 'applications/phragment/controller/PhragmentUpdateController.php',
     'PhragmentVersionController' => 'applications/phragment/controller/PhragmentVersionController.php',
     'PhragmentZIPController' => 'applications/phragment/controller/PhragmentZIPController.php',
     'PhrequentConduitAPIMethod' => 'applications/phrequent/conduit/PhrequentConduitAPIMethod.php',
     'PhrequentController' => 'applications/phrequent/controller/PhrequentController.php',
     'PhrequentDAO' => 'applications/phrequent/storage/PhrequentDAO.php',
     'PhrequentListController' => 'applications/phrequent/controller/PhrequentListController.php',
     'PhrequentPopConduitAPIMethod' => 'applications/phrequent/conduit/PhrequentPopConduitAPIMethod.php',
     'PhrequentPushConduitAPIMethod' => 'applications/phrequent/conduit/PhrequentPushConduitAPIMethod.php',
     'PhrequentSearchEngine' => 'applications/phrequent/query/PhrequentSearchEngine.php',
     'PhrequentTimeBlock' => 'applications/phrequent/storage/PhrequentTimeBlock.php',
     'PhrequentTimeBlockTestCase' => 'applications/phrequent/storage/__tests__/PhrequentTimeBlockTestCase.php',
     'PhrequentTimeSlices' => 'applications/phrequent/storage/PhrequentTimeSlices.php',
     'PhrequentTrackController' => 'applications/phrequent/controller/PhrequentTrackController.php',
     'PhrequentTrackableInterface' => 'applications/phrequent/interface/PhrequentTrackableInterface.php',
     'PhrequentTrackingConduitAPIMethod' => 'applications/phrequent/conduit/PhrequentTrackingConduitAPIMethod.php',
     'PhrequentTrackingEditor' => 'applications/phrequent/editor/PhrequentTrackingEditor.php',
     'PhrequentUIEventListener' => 'applications/phrequent/event/PhrequentUIEventListener.php',
     'PhrequentUserTime' => 'applications/phrequent/storage/PhrequentUserTime.php',
     'PhrequentUserTimeQuery' => 'applications/phrequent/query/PhrequentUserTimeQuery.php',
     'PhrictionChangeType' => 'applications/phriction/constants/PhrictionChangeType.php',
     'PhrictionConduitAPIMethod' => 'applications/phriction/conduit/PhrictionConduitAPIMethod.php',
     'PhrictionConstants' => 'applications/phriction/constants/PhrictionConstants.php',
     'PhrictionContent' => 'applications/phriction/storage/PhrictionContent.php',
     'PhrictionController' => 'applications/phriction/controller/PhrictionController.php',
     'PhrictionCreateConduitAPIMethod' => 'applications/phriction/conduit/PhrictionCreateConduitAPIMethod.php',
     'PhrictionDAO' => 'applications/phriction/storage/PhrictionDAO.php',
     'PhrictionDeleteController' => 'applications/phriction/controller/PhrictionDeleteController.php',
     'PhrictionDiffController' => 'applications/phriction/controller/PhrictionDiffController.php',
     'PhrictionDocument' => 'applications/phriction/storage/PhrictionDocument.php',
     'PhrictionDocumentController' => 'applications/phriction/controller/PhrictionDocumentController.php',
     'PhrictionDocumentHeraldAdapter' => 'applications/phriction/herald/PhrictionDocumentHeraldAdapter.php',
     'PhrictionDocumentPHIDType' => 'applications/phriction/phid/PhrictionDocumentPHIDType.php',
     'PhrictionDocumentQuery' => 'applications/phriction/query/PhrictionDocumentQuery.php',
     'PhrictionDocumentStatus' => 'applications/phriction/constants/PhrictionDocumentStatus.php',
     'PhrictionEditConduitAPIMethod' => 'applications/phriction/conduit/PhrictionEditConduitAPIMethod.php',
     'PhrictionEditController' => 'applications/phriction/controller/PhrictionEditController.php',
     'PhrictionHistoryConduitAPIMethod' => 'applications/phriction/conduit/PhrictionHistoryConduitAPIMethod.php',
     'PhrictionHistoryController' => 'applications/phriction/controller/PhrictionHistoryController.php',
     'PhrictionInfoConduitAPIMethod' => 'applications/phriction/conduit/PhrictionInfoConduitAPIMethod.php',
     'PhrictionListController' => 'applications/phriction/controller/PhrictionListController.php',
     'PhrictionMoveController' => 'applications/phriction/controller/PhrictionMoveController.php',
     'PhrictionNewController' => 'applications/phriction/controller/PhrictionNewController.php',
     'PhrictionRemarkupRule' => 'applications/phriction/markup/PhrictionRemarkupRule.php',
     'PhrictionReplyHandler' => 'applications/phriction/mail/PhrictionReplyHandler.php',
     'PhrictionSchemaSpec' => 'applications/phriction/storage/PhrictionSchemaSpec.php',
     'PhrictionSearchEngine' => 'applications/phriction/query/PhrictionSearchEngine.php',
     'PhrictionSearchIndexer' => 'applications/phriction/search/PhrictionSearchIndexer.php',
     'PhrictionTransaction' => 'applications/phriction/storage/PhrictionTransaction.php',
     'PhrictionTransactionComment' => 'applications/phriction/storage/PhrictionTransactionComment.php',
     'PhrictionTransactionEditor' => 'applications/phriction/editor/PhrictionTransactionEditor.php',
     'PhrictionTransactionQuery' => 'applications/phriction/query/PhrictionTransactionQuery.php',
     'PolicyLockOptionType' => 'applications/policy/config/PolicyLockOptionType.php',
     'PonderAddAnswerView' => 'applications/ponder/view/PonderAddAnswerView.php',
     'PonderAnswer' => 'applications/ponder/storage/PonderAnswer.php',
     'PonderAnswerCommentController' => 'applications/ponder/controller/PonderAnswerCommentController.php',
     'PonderAnswerEditController' => 'applications/ponder/controller/PonderAnswerEditController.php',
     'PonderAnswerEditor' => 'applications/ponder/editor/PonderAnswerEditor.php',
     'PonderAnswerHasVotingUserEdgeType' => 'applications/ponder/edge/PonderAnswerHasVotingUserEdgeType.php',
     'PonderAnswerHistoryController' => 'applications/ponder/controller/PonderAnswerHistoryController.php',
     'PonderAnswerPHIDType' => 'applications/ponder/phid/PonderAnswerPHIDType.php',
     'PonderAnswerQuery' => 'applications/ponder/query/PonderAnswerQuery.php',
     'PonderAnswerSaveController' => 'applications/ponder/controller/PonderAnswerSaveController.php',
     'PonderAnswerTransaction' => 'applications/ponder/storage/PonderAnswerTransaction.php',
     'PonderAnswerTransactionComment' => 'applications/ponder/storage/PonderAnswerTransactionComment.php',
     'PonderAnswerTransactionQuery' => 'applications/ponder/query/PonderAnswerTransactionQuery.php',
     'PonderConstants' => 'applications/ponder/constants/PonderConstants.php',
     'PonderController' => 'applications/ponder/controller/PonderController.php',
     'PonderDAO' => 'applications/ponder/storage/PonderDAO.php',
     'PonderEditor' => 'applications/ponder/editor/PonderEditor.php',
     'PonderQuestion' => 'applications/ponder/storage/PonderQuestion.php',
     'PonderQuestionCommentController' => 'applications/ponder/controller/PonderQuestionCommentController.php',
     'PonderQuestionEditController' => 'applications/ponder/controller/PonderQuestionEditController.php',
     'PonderQuestionEditor' => 'applications/ponder/editor/PonderQuestionEditor.php',
     'PonderQuestionHasVotingUserEdgeType' => 'applications/ponder/edge/PonderQuestionHasVotingUserEdgeType.php',
     'PonderQuestionHistoryController' => 'applications/ponder/controller/PonderQuestionHistoryController.php',
     'PonderQuestionListController' => 'applications/ponder/controller/PonderQuestionListController.php',
     'PonderQuestionMailReceiver' => 'applications/ponder/mail/PonderQuestionMailReceiver.php',
     'PonderQuestionPHIDType' => 'applications/ponder/phid/PonderQuestionPHIDType.php',
     'PonderQuestionQuery' => 'applications/ponder/query/PonderQuestionQuery.php',
     'PonderQuestionReplyHandler' => 'applications/ponder/mail/PonderQuestionReplyHandler.php',
     'PonderQuestionSearchEngine' => 'applications/ponder/query/PonderQuestionSearchEngine.php',
     'PonderQuestionStatus' => 'applications/ponder/constants/PonderQuestionStatus.php',
     'PonderQuestionStatusController' => 'applications/ponder/controller/PonderQuestionStatusController.php',
     'PonderQuestionTransaction' => 'applications/ponder/storage/PonderQuestionTransaction.php',
     'PonderQuestionTransactionComment' => 'applications/ponder/storage/PonderQuestionTransactionComment.php',
     'PonderQuestionTransactionQuery' => 'applications/ponder/query/PonderQuestionTransactionQuery.php',
     'PonderQuestionViewController' => 'applications/ponder/controller/PonderQuestionViewController.php',
     'PonderRemarkupRule' => 'applications/ponder/remarkup/PonderRemarkupRule.php',
     'PonderSchemaSpec' => 'applications/ponder/storage/PonderSchemaSpec.php',
     'PonderSearchIndexer' => 'applications/ponder/search/PonderSearchIndexer.php',
     'PonderTransactionFeedStory' => 'applications/ponder/feed/PonderTransactionFeedStory.php',
     'PonderVotableInterface' => 'applications/ponder/storage/PonderVotableInterface.php',
     'PonderVotableView' => 'applications/ponder/view/PonderVotableView.php',
     'PonderVote' => 'applications/ponder/constants/PonderVote.php',
     'PonderVoteEditor' => 'applications/ponder/editor/PonderVoteEditor.php',
     'PonderVoteSaveController' => 'applications/ponder/controller/PonderVoteSaveController.php',
     'PonderVotingUserHasAnswerEdgeType' => 'applications/ponder/edge/PonderVotingUserHasAnswerEdgeType.php',
     'PonderVotingUserHasQuestionEdgeType' => 'applications/ponder/edge/PonderVotingUserHasQuestionEdgeType.php',
     'ProjectAddProjectsEmailCommand' => 'applications/project/command/ProjectAddProjectsEmailCommand.php',
     'ProjectBoardTaskCard' => 'applications/project/view/ProjectBoardTaskCard.php',
     'ProjectCanLockProjectsCapability' => 'applications/project/capability/ProjectCanLockProjectsCapability.php',
     'ProjectConduitAPIMethod' => 'applications/project/conduit/ProjectConduitAPIMethod.php',
     'ProjectCreateConduitAPIMethod' => 'applications/project/conduit/ProjectCreateConduitAPIMethod.php',
     'ProjectCreateProjectsCapability' => 'applications/project/capability/ProjectCreateProjectsCapability.php',
     'ProjectDefaultEditCapability' => 'applications/project/capability/ProjectDefaultEditCapability.php',
     'ProjectDefaultJoinCapability' => 'applications/project/capability/ProjectDefaultJoinCapability.php',
     'ProjectDefaultViewCapability' => 'applications/project/capability/ProjectDefaultViewCapability.php',
     'ProjectQueryConduitAPIMethod' => 'applications/project/conduit/ProjectQueryConduitAPIMethod.php',
     'ProjectRemarkupRule' => 'applications/project/remarkup/ProjectRemarkupRule.php',
     'ProjectRemarkupRuleTestCase' => 'applications/project/remarkup/__tests__/ProjectRemarkupRuleTestCase.php',
     'ProjectReplyHandler' => 'applications/project/mail/ProjectReplyHandler.php',
     'QueryFormattingTestCase' => 'infrastructure/storage/__tests__/QueryFormattingTestCase.php',
     'ReleephAuthorFieldSpecification' => 'applications/releeph/field/specification/ReleephAuthorFieldSpecification.php',
     'ReleephBranch' => 'applications/releeph/storage/ReleephBranch.php',
     'ReleephBranchAccessController' => 'applications/releeph/controller/branch/ReleephBranchAccessController.php',
     'ReleephBranchCommitFieldSpecification' => 'applications/releeph/field/specification/ReleephBranchCommitFieldSpecification.php',
     'ReleephBranchController' => 'applications/releeph/controller/branch/ReleephBranchController.php',
     'ReleephBranchCreateController' => 'applications/releeph/controller/branch/ReleephBranchCreateController.php',
     'ReleephBranchEditController' => 'applications/releeph/controller/branch/ReleephBranchEditController.php',
     'ReleephBranchEditor' => 'applications/releeph/editor/ReleephBranchEditor.php',
     'ReleephBranchHistoryController' => 'applications/releeph/controller/branch/ReleephBranchHistoryController.php',
     'ReleephBranchNamePreviewController' => 'applications/releeph/controller/branch/ReleephBranchNamePreviewController.php',
     'ReleephBranchPHIDType' => 'applications/releeph/phid/ReleephBranchPHIDType.php',
     'ReleephBranchPreviewView' => 'applications/releeph/view/branch/ReleephBranchPreviewView.php',
     'ReleephBranchQuery' => 'applications/releeph/query/ReleephBranchQuery.php',
     'ReleephBranchSearchEngine' => 'applications/releeph/query/ReleephBranchSearchEngine.php',
     'ReleephBranchTemplate' => 'applications/releeph/view/branch/ReleephBranchTemplate.php',
     'ReleephBranchTransaction' => 'applications/releeph/storage/ReleephBranchTransaction.php',
     'ReleephBranchTransactionQuery' => 'applications/releeph/query/ReleephBranchTransactionQuery.php',
     'ReleephBranchViewController' => 'applications/releeph/controller/branch/ReleephBranchViewController.php',
     'ReleephCommitFinder' => 'applications/releeph/commitfinder/ReleephCommitFinder.php',
     'ReleephCommitFinderException' => 'applications/releeph/commitfinder/ReleephCommitFinderException.php',
     'ReleephCommitMessageFieldSpecification' => 'applications/releeph/field/specification/ReleephCommitMessageFieldSpecification.php',
     'ReleephConduitAPIMethod' => 'applications/releeph/conduit/ReleephConduitAPIMethod.php',
     'ReleephController' => 'applications/releeph/controller/ReleephController.php',
     'ReleephDAO' => 'applications/releeph/storage/ReleephDAO.php',
     'ReleephDefaultFieldSelector' => 'applications/releeph/field/selector/ReleephDefaultFieldSelector.php',
     'ReleephDependsOnFieldSpecification' => 'applications/releeph/field/specification/ReleephDependsOnFieldSpecification.php',
     'ReleephDiffChurnFieldSpecification' => 'applications/releeph/field/specification/ReleephDiffChurnFieldSpecification.php',
     'ReleephDiffMessageFieldSpecification' => 'applications/releeph/field/specification/ReleephDiffMessageFieldSpecification.php',
     'ReleephDiffSizeFieldSpecification' => 'applications/releeph/field/specification/ReleephDiffSizeFieldSpecification.php',
     'ReleephFieldParseException' => 'applications/releeph/field/exception/ReleephFieldParseException.php',
     'ReleephFieldSelector' => 'applications/releeph/field/selector/ReleephFieldSelector.php',
     'ReleephFieldSpecification' => 'applications/releeph/field/specification/ReleephFieldSpecification.php',
     'ReleephGetBranchesConduitAPIMethod' => 'applications/releeph/conduit/ReleephGetBranchesConduitAPIMethod.php',
     'ReleephIntentFieldSpecification' => 'applications/releeph/field/specification/ReleephIntentFieldSpecification.php',
     'ReleephLevelFieldSpecification' => 'applications/releeph/field/specification/ReleephLevelFieldSpecification.php',
     'ReleephOriginalCommitFieldSpecification' => 'applications/releeph/field/specification/ReleephOriginalCommitFieldSpecification.php',
     'ReleephProductActionController' => 'applications/releeph/controller/product/ReleephProductActionController.php',
     'ReleephProductController' => 'applications/releeph/controller/product/ReleephProductController.php',
     'ReleephProductCreateController' => 'applications/releeph/controller/product/ReleephProductCreateController.php',
     'ReleephProductEditController' => 'applications/releeph/controller/product/ReleephProductEditController.php',
     'ReleephProductEditor' => 'applications/releeph/editor/ReleephProductEditor.php',
     'ReleephProductHistoryController' => 'applications/releeph/controller/product/ReleephProductHistoryController.php',
     'ReleephProductListController' => 'applications/releeph/controller/product/ReleephProductListController.php',
     'ReleephProductPHIDType' => 'applications/releeph/phid/ReleephProductPHIDType.php',
     'ReleephProductQuery' => 'applications/releeph/query/ReleephProductQuery.php',
     'ReleephProductSearchEngine' => 'applications/releeph/query/ReleephProductSearchEngine.php',
     'ReleephProductTransaction' => 'applications/releeph/storage/ReleephProductTransaction.php',
     'ReleephProductTransactionQuery' => 'applications/releeph/query/ReleephProductTransactionQuery.php',
     'ReleephProductViewController' => 'applications/releeph/controller/product/ReleephProductViewController.php',
     'ReleephProject' => 'applications/releeph/storage/ReleephProject.php',
     'ReleephQueryBranchesConduitAPIMethod' => 'applications/releeph/conduit/ReleephQueryBranchesConduitAPIMethod.php',
     'ReleephQueryProductsConduitAPIMethod' => 'applications/releeph/conduit/ReleephQueryProductsConduitAPIMethod.php',
     'ReleephQueryRequestsConduitAPIMethod' => 'applications/releeph/conduit/ReleephQueryRequestsConduitAPIMethod.php',
     'ReleephReasonFieldSpecification' => 'applications/releeph/field/specification/ReleephReasonFieldSpecification.php',
     'ReleephRequest' => 'applications/releeph/storage/ReleephRequest.php',
     'ReleephRequestActionController' => 'applications/releeph/controller/request/ReleephRequestActionController.php',
     'ReleephRequestCommentController' => 'applications/releeph/controller/request/ReleephRequestCommentController.php',
     'ReleephRequestConduitAPIMethod' => 'applications/releeph/conduit/ReleephRequestConduitAPIMethod.php',
     'ReleephRequestController' => 'applications/releeph/controller/request/ReleephRequestController.php',
     'ReleephRequestDifferentialCreateController' => 'applications/releeph/controller/request/ReleephRequestDifferentialCreateController.php',
     'ReleephRequestEditController' => 'applications/releeph/controller/request/ReleephRequestEditController.php',
     'ReleephRequestMailReceiver' => 'applications/releeph/mail/ReleephRequestMailReceiver.php',
     'ReleephRequestPHIDType' => 'applications/releeph/phid/ReleephRequestPHIDType.php',
     'ReleephRequestQuery' => 'applications/releeph/query/ReleephRequestQuery.php',
     'ReleephRequestReplyHandler' => 'applications/releeph/mail/ReleephRequestReplyHandler.php',
     'ReleephRequestSearchEngine' => 'applications/releeph/query/ReleephRequestSearchEngine.php',
     'ReleephRequestStatus' => 'applications/releeph/constants/ReleephRequestStatus.php',
     'ReleephRequestTransaction' => 'applications/releeph/storage/ReleephRequestTransaction.php',
     'ReleephRequestTransactionComment' => 'applications/releeph/storage/ReleephRequestTransactionComment.php',
     'ReleephRequestTransactionQuery' => 'applications/releeph/query/ReleephRequestTransactionQuery.php',
     'ReleephRequestTransactionalEditor' => 'applications/releeph/editor/ReleephRequestTransactionalEditor.php',
     'ReleephRequestTypeaheadControl' => 'applications/releeph/view/request/ReleephRequestTypeaheadControl.php',
     'ReleephRequestTypeaheadController' => 'applications/releeph/controller/request/ReleephRequestTypeaheadController.php',
     'ReleephRequestView' => 'applications/releeph/view/ReleephRequestView.php',
     'ReleephRequestViewController' => 'applications/releeph/controller/request/ReleephRequestViewController.php',
     'ReleephRequestorFieldSpecification' => 'applications/releeph/field/specification/ReleephRequestorFieldSpecification.php',
     'ReleephRevisionFieldSpecification' => 'applications/releeph/field/specification/ReleephRevisionFieldSpecification.php',
     'ReleephSeverityFieldSpecification' => 'applications/releeph/field/specification/ReleephSeverityFieldSpecification.php',
     'ReleephSummaryFieldSpecification' => 'applications/releeph/field/specification/ReleephSummaryFieldSpecification.php',
     'ReleephWorkCanPushConduitAPIMethod' => 'applications/releeph/conduit/work/ReleephWorkCanPushConduitAPIMethod.php',
     'ReleephWorkGetAuthorInfoConduitAPIMethod' => 'applications/releeph/conduit/work/ReleephWorkGetAuthorInfoConduitAPIMethod.php',
     'ReleephWorkGetBranchCommitMessageConduitAPIMethod' => 'applications/releeph/conduit/work/ReleephWorkGetBranchCommitMessageConduitAPIMethod.php',
     'ReleephWorkGetBranchConduitAPIMethod' => 'applications/releeph/conduit/work/ReleephWorkGetBranchConduitAPIMethod.php',
     'ReleephWorkGetCommitMessageConduitAPIMethod' => 'applications/releeph/conduit/work/ReleephWorkGetCommitMessageConduitAPIMethod.php',
     'ReleephWorkNextRequestConduitAPIMethod' => 'applications/releeph/conduit/work/ReleephWorkNextRequestConduitAPIMethod.php',
     'ReleephWorkRecordConduitAPIMethod' => 'applications/releeph/conduit/work/ReleephWorkRecordConduitAPIMethod.php',
     'ReleephWorkRecordPickStatusConduitAPIMethod' => 'applications/releeph/conduit/work/ReleephWorkRecordPickStatusConduitAPIMethod.php',
     'RemarkupProcessConduitAPIMethod' => 'applications/remarkup/conduit/RemarkupProcessConduitAPIMethod.php',
     'RepositoryConduitAPIMethod' => 'applications/repository/conduit/RepositoryConduitAPIMethod.php',
     'RepositoryCreateConduitAPIMethod' => 'applications/repository/conduit/RepositoryCreateConduitAPIMethod.php',
     'RepositoryQueryConduitAPIMethod' => 'applications/repository/conduit/RepositoryQueryConduitAPIMethod.php',
     'ShellLogView' => 'applications/harbormaster/view/ShellLogView.php',
     'SlowvoteConduitAPIMethod' => 'applications/slowvote/conduit/SlowvoteConduitAPIMethod.php',
     'SlowvoteEmbedView' => 'applications/slowvote/view/SlowvoteEmbedView.php',
     'SlowvoteInfoConduitAPIMethod' => 'applications/slowvote/conduit/SlowvoteInfoConduitAPIMethod.php',
     'SlowvoteRemarkupRule' => 'applications/slowvote/remarkup/SlowvoteRemarkupRule.php',
     'SubscriptionListDialogBuilder' => 'applications/subscriptions/view/SubscriptionListDialogBuilder.php',
     'SubscriptionListStringBuilder' => 'applications/subscriptions/view/SubscriptionListStringBuilder.php',
     'TokenConduitAPIMethod' => 'applications/tokens/conduit/TokenConduitAPIMethod.php',
     'TokenGiveConduitAPIMethod' => 'applications/tokens/conduit/TokenGiveConduitAPIMethod.php',
     'TokenGivenConduitAPIMethod' => 'applications/tokens/conduit/TokenGivenConduitAPIMethod.php',
     'TokenQueryConduitAPIMethod' => 'applications/tokens/conduit/TokenQueryConduitAPIMethod.php',
     'UserConduitAPIMethod' => 'applications/people/conduit/UserConduitAPIMethod.php',
     'UserDisableConduitAPIMethod' => 'applications/people/conduit/UserDisableConduitAPIMethod.php',
     'UserEnableConduitAPIMethod' => 'applications/people/conduit/UserEnableConduitAPIMethod.php',
     'UserFindConduitAPIMethod' => 'applications/people/conduit/UserFindConduitAPIMethod.php',
     'UserQueryConduitAPIMethod' => 'applications/people/conduit/UserQueryConduitAPIMethod.php',
     'UserWhoAmIConduitAPIMethod' => 'applications/people/conduit/UserWhoAmIConduitAPIMethod.php',
   ),
   'function' => array(
     'celerity_generate_unique_node_id' => 'applications/celerity/api.php',
     'celerity_get_resource_uri' => 'applications/celerity/api.php',
     'javelin_tag' => 'infrastructure/javelin/markup.php',
     'phabricator_date' => 'view/viewutils.php',
     'phabricator_datetime' => 'view/viewutils.php',
     'phabricator_form' => 'infrastructure/javelin/markup.php',
     'phabricator_format_local_time' => 'view/viewutils.php',
     'phabricator_relative_date' => 'view/viewutils.php',
     'phabricator_time' => 'view/viewutils.php',
     'phabricator_time_format' => 'view/viewutils.php',
     'phid_get_subtype' => 'applications/phid/utils.php',
     'phid_get_type' => 'applications/phid/utils.php',
     'phid_group_by_type' => 'applications/phid/utils.php',
     'require_celerity_resource' => 'applications/celerity/api.php',
   ),
   'xmap' => array(
     'AlmanacAddress' => 'Phobject',
     'AlmanacBinding' => array(
       'AlmanacDAO',
       'PhabricatorPolicyInterface',
       'PhabricatorCustomFieldInterface',
       'PhabricatorApplicationTransactionInterface',
       'AlmanacPropertyInterface',
     ),
     'AlmanacBindingEditController' => 'AlmanacServiceController',
     'AlmanacBindingEditor' => 'PhabricatorApplicationTransactionEditor',
     'AlmanacBindingPHIDType' => 'PhabricatorPHIDType',
     'AlmanacBindingQuery' => 'AlmanacQuery',
     'AlmanacBindingTableView' => 'AphrontView',
     'AlmanacBindingTransaction' => 'PhabricatorApplicationTransaction',
     'AlmanacBindingTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
     'AlmanacBindingViewController' => 'AlmanacServiceController',
     'AlmanacClusterDatabaseServiceType' => 'AlmanacClusterServiceType',
     'AlmanacClusterRepositoryServiceType' => 'AlmanacClusterServiceType',
     'AlmanacClusterServiceType' => 'AlmanacServiceType',
     'AlmanacConduitAPIMethod' => 'ConduitAPIMethod',
     'AlmanacConsoleController' => 'AlmanacController',
     'AlmanacController' => 'PhabricatorController',
     'AlmanacCoreCustomField' => array(
       'AlmanacCustomField',
       'PhabricatorStandardCustomFieldInterface',
     ),
     'AlmanacCreateClusterServicesCapability' => 'PhabricatorPolicyCapability',
     'AlmanacCreateDevicesCapability' => 'PhabricatorPolicyCapability',
     'AlmanacCreateNetworksCapability' => 'PhabricatorPolicyCapability',
     'AlmanacCreateServicesCapability' => 'PhabricatorPolicyCapability',
     'AlmanacCustomField' => 'PhabricatorCustomField',
     'AlmanacCustomServiceType' => 'AlmanacServiceType',
     'AlmanacDAO' => 'PhabricatorLiskDAO',
     'AlmanacDevice' => array(
       'AlmanacDAO',
       'PhabricatorPolicyInterface',
       'PhabricatorCustomFieldInterface',
       'PhabricatorApplicationTransactionInterface',
       'PhabricatorProjectInterface',
       'PhabricatorSSHPublicKeyInterface',
       'AlmanacPropertyInterface',
       'PhabricatorDestructibleInterface',
     ),
     'AlmanacDeviceController' => 'AlmanacController',
     'AlmanacDeviceEditController' => 'AlmanacDeviceController',
     'AlmanacDeviceEditor' => 'PhabricatorApplicationTransactionEditor',
     'AlmanacDeviceListController' => 'AlmanacDeviceController',
     'AlmanacDevicePHIDType' => 'PhabricatorPHIDType',
     'AlmanacDeviceQuery' => 'AlmanacQuery',
     'AlmanacDeviceSearchEngine' => 'PhabricatorApplicationSearchEngine',
     'AlmanacDeviceTransaction' => 'PhabricatorApplicationTransaction',
     'AlmanacDeviceTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
     'AlmanacDeviceViewController' => 'AlmanacDeviceController',
     'AlmanacInterface' => array(
       'AlmanacDAO',
       'PhabricatorPolicyInterface',
       'PhabricatorDestructibleInterface',
     ),
     'AlmanacInterfaceDatasource' => 'PhabricatorTypeaheadDatasource',
     'AlmanacInterfaceEditController' => 'AlmanacDeviceController',
     'AlmanacInterfacePHIDType' => 'PhabricatorPHIDType',
     'AlmanacInterfaceQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'AlmanacInterfaceTableView' => 'AphrontView',
     'AlmanacKeys' => 'Phobject',
     'AlmanacManagementLockWorkflow' => 'AlmanacManagementWorkflow',
     'AlmanacManagementRegisterWorkflow' => 'AlmanacManagementWorkflow',
     'AlmanacManagementTrustKeyWorkflow' => 'AlmanacManagementWorkflow',
     'AlmanacManagementUnlockWorkflow' => 'AlmanacManagementWorkflow',
     'AlmanacManagementUntrustKeyWorkflow' => 'AlmanacManagementWorkflow',
     'AlmanacManagementWorkflow' => 'PhabricatorManagementWorkflow',
     'AlmanacNames' => 'Phobject',
     'AlmanacNamesTestCase' => 'PhabricatorTestCase',
     'AlmanacNetwork' => array(
       'AlmanacDAO',
       'PhabricatorApplicationTransactionInterface',
       'PhabricatorPolicyInterface',
       'PhabricatorDestructibleInterface',
     ),
     'AlmanacNetworkController' => 'AlmanacController',
     'AlmanacNetworkEditController' => 'AlmanacNetworkController',
     'AlmanacNetworkEditor' => 'PhabricatorApplicationTransactionEditor',
     'AlmanacNetworkListController' => 'AlmanacNetworkController',
     'AlmanacNetworkPHIDType' => 'PhabricatorPHIDType',
     'AlmanacNetworkQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'AlmanacNetworkSearchEngine' => 'PhabricatorApplicationSearchEngine',
     'AlmanacNetworkTransaction' => 'PhabricatorApplicationTransaction',
     'AlmanacNetworkTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
     'AlmanacNetworkViewController' => 'AlmanacNetworkController',
     'AlmanacProperty' => array(
       'PhabricatorCustomFieldStorage',
       'PhabricatorPolicyInterface',
     ),
     'AlmanacPropertyController' => 'AlmanacController',
     'AlmanacPropertyDeleteController' => 'AlmanacDeviceController',
     'AlmanacPropertyEditController' => 'AlmanacDeviceController',
     'AlmanacPropertyQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'AlmanacQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'AlmanacQueryDevicesConduitAPIMethod' => 'AlmanacConduitAPIMethod',
     'AlmanacQueryServicesConduitAPIMethod' => 'AlmanacConduitAPIMethod',
     'AlmanacSchemaSpec' => 'PhabricatorConfigSchemaSpec',
     'AlmanacService' => array(
       'AlmanacDAO',
       'PhabricatorPolicyInterface',
       'PhabricatorCustomFieldInterface',
       'PhabricatorApplicationTransactionInterface',
       'PhabricatorProjectInterface',
       'AlmanacPropertyInterface',
       'PhabricatorDestructibleInterface',
     ),
     'AlmanacServiceController' => 'AlmanacController',
     'AlmanacServiceDatasource' => 'PhabricatorTypeaheadDatasource',
     'AlmanacServiceEditController' => 'AlmanacServiceController',
     'AlmanacServiceEditor' => 'PhabricatorApplicationTransactionEditor',
     'AlmanacServiceListController' => 'AlmanacServiceController',
     'AlmanacServicePHIDType' => 'PhabricatorPHIDType',
     'AlmanacServiceQuery' => 'AlmanacQuery',
     'AlmanacServiceSearchEngine' => 'PhabricatorApplicationSearchEngine',
     'AlmanacServiceTransaction' => 'PhabricatorApplicationTransaction',
     'AlmanacServiceTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
     'AlmanacServiceType' => 'Phobject',
+    'AlmanacServiceTypeTestCase' => 'PhabricatorTestCase',
     'AlmanacServiceViewController' => 'AlmanacServiceController',
+    'AphlictDropdownDataQuery' => 'Phobject',
     'Aphront304Response' => 'AphrontResponse',
     'Aphront400Response' => 'AphrontResponse',
     'Aphront403Response' => 'AphrontHTMLResponse',
     'Aphront404Response' => 'AphrontHTMLResponse',
     'AphrontAjaxResponse' => 'AphrontResponse',
+    'AphrontApplicationConfiguration' => 'Phobject',
     'AphrontBarView' => 'AphrontView',
     'AphrontCSRFException' => 'AphrontException',
     'AphrontCalendarEventView' => 'AphrontView',
     'AphrontController' => 'Phobject',
     'AphrontCursorPagerView' => 'AphrontView',
     'AphrontDefaultApplicationConfiguration' => 'AphrontApplicationConfiguration',
     'AphrontDialogResponse' => 'AphrontResponse',
     'AphrontDialogView' => 'AphrontView',
     'AphrontException' => 'Exception',
     'AphrontFileResponse' => 'AphrontResponse',
     'AphrontFormCheckboxControl' => 'AphrontFormControl',
     'AphrontFormChooseButtonControl' => 'AphrontFormControl',
     'AphrontFormControl' => 'AphrontView',
     'AphrontFormDateControl' => 'AphrontFormControl',
     'AphrontFormDateControlValue' => 'Phobject',
     'AphrontFormDividerControl' => 'AphrontFormControl',
     'AphrontFormFileControl' => 'AphrontFormControl',
     'AphrontFormMarkupControl' => 'AphrontFormControl',
     'AphrontFormPasswordControl' => 'AphrontFormControl',
     'AphrontFormPolicyControl' => 'AphrontFormControl',
     'AphrontFormRadioButtonControl' => 'AphrontFormControl',
     'AphrontFormRecaptchaControl' => 'AphrontFormControl',
     'AphrontFormSelectControl' => 'AphrontFormControl',
     'AphrontFormStaticControl' => 'AphrontFormControl',
     'AphrontFormSubmitControl' => 'AphrontFormControl',
     'AphrontFormTextAreaControl' => 'AphrontFormControl',
     'AphrontFormTextControl' => 'AphrontFormControl',
     'AphrontFormTextWithSubmitControl' => 'AphrontFormControl',
     'AphrontFormTokenizerControl' => 'AphrontFormControl',
     'AphrontFormTypeaheadControl' => 'AphrontFormControl',
     'AphrontFormView' => 'AphrontView',
     'AphrontGlyphBarView' => 'AphrontBarView',
     'AphrontHTMLResponse' => 'AphrontResponse',
     'AphrontHTTPProxyResponse' => 'AphrontResponse',
+    'AphrontHTTPSink' => 'Phobject',
     'AphrontHTTPSinkTestCase' => 'PhabricatorTestCase',
     'AphrontIsolatedDatabaseConnectionTestCase' => 'PhabricatorTestCase',
     'AphrontIsolatedHTTPSink' => 'AphrontHTTPSink',
     'AphrontJSONResponse' => 'AphrontResponse',
     'AphrontJavelinView' => 'AphrontView',
     'AphrontKeyboardShortcutsAvailableView' => 'AphrontView',
     'AphrontListFilterView' => 'AphrontView',
     'AphrontMoreView' => 'AphrontView',
     'AphrontMultiColumnView' => 'AphrontView',
     'AphrontMySQLDatabaseConnectionTestCase' => 'PhabricatorTestCase',
     'AphrontNullView' => 'AphrontView',
     'AphrontPHPHTTPSink' => 'AphrontHTTPSink',
     'AphrontPageView' => 'AphrontView',
     'AphrontPlainTextResponse' => 'AphrontResponse',
     'AphrontProgressBarView' => 'AphrontBarView',
     'AphrontProxyResponse' => 'AphrontResponse',
     'AphrontRedirectResponse' => 'AphrontResponse',
     'AphrontRedirectResponseTestCase' => 'PhabricatorTestCase',
     'AphrontReloadResponse' => 'AphrontRedirectResponse',
+    'AphrontRequest' => 'Phobject',
     'AphrontRequestTestCase' => 'PhabricatorTestCase',
+    'AphrontResponse' => 'Phobject',
     'AphrontSideNavFilterView' => 'AphrontView',
     'AphrontStackTraceView' => 'AphrontView',
     'AphrontStandaloneHTMLResponse' => 'AphrontHTMLResponse',
     'AphrontTableView' => 'AphrontView',
     'AphrontTagView' => 'AphrontView',
     'AphrontTokenizerTemplateView' => 'AphrontView',
     'AphrontTwoColumnView' => 'AphrontView',
     'AphrontTypeaheadTemplateView' => 'AphrontView',
+    'AphrontURIMapper' => 'Phobject',
     'AphrontUnhandledExceptionResponse' => 'AphrontStandaloneHTMLResponse',
     'AphrontUsageException' => 'AphrontException',
     'AphrontView' => array(
       'Phobject',
       'PhutilSafeHTMLProducerInterface',
     ),
     'AphrontWebpageResponse' => 'AphrontHTMLResponse',
     'ArcanistConduitAPIMethod' => 'ConduitAPIMethod',
     'ArcanistProjectInfoConduitAPIMethod' => 'ArcanistConduitAPIMethod',
     'AuditConduitAPIMethod' => 'ConduitAPIMethod',
     'AuditQueryConduitAPIMethod' => 'AuditConduitAPIMethod',
     'AuthManageProvidersCapability' => 'PhabricatorPolicyCapability',
+    'CalendarTimeUtil' => 'Phobject',
     'CalendarTimeUtilTestCase' => 'PhabricatorTestCase',
+    'CelerityAPI' => 'Phobject',
     'CelerityManagementMapWorkflow' => 'CelerityManagementWorkflow',
     'CelerityManagementWorkflow' => 'PhabricatorManagementWorkflow',
     'CelerityPhabricatorResourceController' => 'CelerityResourceController',
     'CelerityPhabricatorResources' => 'CelerityResourcesOnDisk',
     'CelerityPhysicalResources' => 'CelerityResources',
+    'CelerityPhysicalResourcesTestCase' => 'PhabricatorTestCase',
     'CelerityResourceController' => 'PhabricatorController',
     'CelerityResourceGraph' => 'AbstractDirectedGraph',
+    'CelerityResourceMap' => 'Phobject',
+    'CelerityResourceMapGenerator' => 'Phobject',
+    'CelerityResourceTransformer' => 'Phobject',
     'CelerityResourceTransformerTestCase' => 'PhabricatorTestCase',
+    'CelerityResources' => 'Phobject',
     'CelerityResourcesOnDisk' => 'CelerityPhysicalResources',
+    'CeleritySpriteGenerator' => 'Phobject',
+    'CelerityStaticResourceResponse' => 'Phobject',
     'ChatLogConduitAPIMethod' => 'ConduitAPIMethod',
     'ChatLogQueryConduitAPIMethod' => 'ChatLogConduitAPIMethod',
     'ChatLogRecordConduitAPIMethod' => 'ChatLogConduitAPIMethod',
     'ConduitAPIMethod' => array(
       'Phobject',
       'PhabricatorPolicyInterface',
     ),
+    'ConduitAPIMethodTestCase' => 'PhabricatorTestCase',
+    'ConduitAPIRequest' => 'Phobject',
+    'ConduitAPIResponse' => 'Phobject',
     'ConduitApplicationNotInstalledException' => 'ConduitMethodNotFoundException',
+    'ConduitCall' => 'Phobject',
     'ConduitCallTestCase' => 'PhabricatorTestCase',
     'ConduitConnectConduitAPIMethod' => 'ConduitAPIMethod',
     'ConduitConnectionGarbageCollector' => 'PhabricatorGarbageCollector',
     'ConduitDeprecatedCallSetupCheck' => 'PhabricatorSetupCheck',
     'ConduitException' => 'Exception',
     'ConduitGetCapabilitiesConduitAPIMethod' => 'ConduitAPIMethod',
     'ConduitGetCertificateConduitAPIMethod' => 'ConduitAPIMethod',
     'ConduitLogGarbageCollector' => 'PhabricatorGarbageCollector',
     'ConduitMethodDoesNotExistException' => 'ConduitMethodNotFoundException',
     'ConduitMethodNotFoundException' => 'ConduitException',
     'ConduitPingConduitAPIMethod' => 'ConduitAPIMethod',
     'ConduitQueryConduitAPIMethod' => 'ConduitAPIMethod',
     'ConduitSSHWorkflow' => 'PhabricatorSSHWorkflow',
     'ConduitTokenGarbageCollector' => 'PhabricatorGarbageCollector',
     'ConpherenceColumnViewController' => 'ConpherenceController',
     'ConpherenceConduitAPIMethod' => 'ConduitAPIMethod',
     'ConpherenceConfigOptions' => 'PhabricatorApplicationConfigOptions',
+    'ConpherenceConstants' => 'Phobject',
     'ConpherenceController' => 'PhabricatorController',
     'ConpherenceCreateThreadConduitAPIMethod' => 'ConpherenceConduitAPIMethod',
     'ConpherenceCreateThreadMailReceiver' => 'PhabricatorMailReceiver',
     'ConpherenceDAO' => 'PhabricatorLiskDAO',
     'ConpherenceDurableColumnView' => 'AphrontTagView',
     'ConpherenceEditor' => 'PhabricatorApplicationTransactionEditor',
     'ConpherenceFileWidgetView' => 'ConpherenceWidgetView',
     'ConpherenceFormDragAndDropUploadControl' => 'AphrontFormControl',
     'ConpherenceFulltextQuery' => 'PhabricatorOffsetPagedQuery',
     'ConpherenceHovercardEventListener' => 'PhabricatorEventListener',
     'ConpherenceImageData' => 'ConpherenceConstants',
     'ConpherenceIndex' => 'ConpherenceDAO',
     'ConpherenceLayoutView' => 'AphrontView',
     'ConpherenceListController' => 'ConpherenceController',
     'ConpherenceMenuItemView' => 'AphrontTagView',
     'ConpherenceNewController' => 'ConpherenceController',
     'ConpherenceNewRoomController' => 'ConpherenceController',
     'ConpherenceNotificationPanelController' => 'ConpherenceController',
     'ConpherenceParticipant' => 'ConpherenceDAO',
     'ConpherenceParticipantCountQuery' => 'PhabricatorOffsetPagedQuery',
     'ConpherenceParticipantQuery' => 'PhabricatorOffsetPagedQuery',
     'ConpherenceParticipationStatus' => 'ConpherenceConstants',
     'ConpherencePeopleWidgetView' => 'ConpherenceWidgetView',
     'ConpherencePicCropControl' => 'AphrontFormControl',
     'ConpherenceQueryThreadConduitAPIMethod' => 'ConpherenceConduitAPIMethod',
     'ConpherenceQueryTransactionConduitAPIMethod' => 'ConpherenceConduitAPIMethod',
     'ConpherenceReplyHandler' => 'PhabricatorMailReplyHandler',
     'ConpherenceRoomListController' => 'ConpherenceController',
     'ConpherenceRoomTestCase' => 'ConpherenceTestCase',
     'ConpherenceSchemaSpec' => 'PhabricatorConfigSchemaSpec',
     'ConpherenceSettings' => 'ConpherenceConstants',
     'ConpherenceTestCase' => 'PhabricatorTestCase',
     'ConpherenceThread' => array(
       'ConpherenceDAO',
       'PhabricatorPolicyInterface',
       'PhabricatorApplicationTransactionInterface',
       'PhabricatorMentionableInterface',
       'PhabricatorDestructibleInterface',
     ),
     'ConpherenceThreadIndexer' => 'PhabricatorSearchDocumentIndexer',
     'ConpherenceThreadListView' => 'AphrontView',
     'ConpherenceThreadMailReceiver' => 'PhabricatorObjectMailReceiver',
+    'ConpherenceThreadMembersPolicyRule' => 'PhabricatorPolicyRule',
     'ConpherenceThreadQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'ConpherenceThreadRemarkupRule' => 'PhabricatorObjectRemarkupRule',
     'ConpherenceThreadSearchEngine' => 'PhabricatorApplicationSearchEngine',
     'ConpherenceThreadTestCase' => 'ConpherenceTestCase',
     'ConpherenceTransaction' => 'PhabricatorApplicationTransaction',
     'ConpherenceTransactionComment' => 'PhabricatorApplicationTransactionComment',
     'ConpherenceTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
+    'ConpherenceTransactionRenderer' => 'Phobject',
     'ConpherenceTransactionView' => 'AphrontView',
     'ConpherenceUpdateActions' => 'ConpherenceConstants',
     'ConpherenceUpdateController' => 'ConpherenceController',
     'ConpherenceUpdateThreadConduitAPIMethod' => 'ConpherenceConduitAPIMethod',
     'ConpherenceViewController' => 'ConpherenceController',
     'ConpherenceWidgetConfigConstants' => 'ConpherenceConstants',
     'ConpherenceWidgetController' => 'ConpherenceController',
     'ConpherenceWidgetView' => 'AphrontView',
     'DarkConsoleController' => 'PhabricatorController',
+    'DarkConsoleCore' => 'Phobject',
     'DarkConsoleDataController' => 'PhabricatorController',
     'DarkConsoleErrorLogPlugin' => 'DarkConsolePlugin',
+    'DarkConsoleErrorLogPluginAPI' => 'Phobject',
     'DarkConsoleEventPlugin' => 'DarkConsolePlugin',
     'DarkConsoleEventPluginAPI' => 'PhabricatorEventListener',
+    'DarkConsolePlugin' => 'Phobject',
     'DarkConsoleRequestPlugin' => 'DarkConsolePlugin',
     'DarkConsoleServicesPlugin' => 'DarkConsolePlugin',
     'DarkConsoleXHProfPlugin' => 'DarkConsolePlugin',
-    'DefaultDatabaseConfigurationProvider' => 'DatabaseConfigurationProvider',
+    'DarkConsoleXHProfPluginAPI' => 'Phobject',
+    'DefaultDatabaseConfigurationProvider' => array(
+      'Phobject',
+      'DatabaseConfigurationProvider',
+    ),
+    'DifferentialAction' => 'Phobject',
     'DifferentialActionEmailCommand' => 'MetaMTAEmailTransactionCommand',
     'DifferentialActionMenuEventListener' => 'PhabricatorEventListener',
     'DifferentialAddCommentView' => 'AphrontView',
     'DifferentialAdjustmentMapTestCase' => 'PhutilTestCase',
     'DifferentialAffectedPath' => 'DifferentialDAO',
     'DifferentialApplyPatchField' => 'DifferentialCustomField',
     'DifferentialAsanaRepresentationField' => 'DifferentialCustomField',
     'DifferentialAuditorsField' => 'DifferentialStoredCustomField',
     'DifferentialAuthorField' => 'DifferentialCustomField',
     'DifferentialBlameRevisionField' => 'DifferentialStoredCustomField',
     'DifferentialBranchField' => 'DifferentialCustomField',
+    'DifferentialChangeType' => 'Phobject',
     'DifferentialChangesSinceLastUpdateField' => 'DifferentialCustomField',
     'DifferentialChangeset' => array(
       'DifferentialDAO',
       'PhabricatorPolicyInterface',
     ),
     'DifferentialChangesetDetailView' => 'AphrontView',
+    'DifferentialChangesetFileTreeSideNavBuilder' => 'Phobject',
     'DifferentialChangesetHTMLRenderer' => 'DifferentialChangesetRenderer',
     'DifferentialChangesetListView' => 'AphrontView',
     'DifferentialChangesetOneUpRenderer' => 'DifferentialChangesetHTMLRenderer',
     'DifferentialChangesetOneUpTestRenderer' => 'DifferentialChangesetTestRenderer',
+    'DifferentialChangesetParser' => 'Phobject',
     'DifferentialChangesetParserTestCase' => 'PhabricatorTestCase',
     'DifferentialChangesetQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
+    'DifferentialChangesetRenderer' => 'Phobject',
     'DifferentialChangesetTestRenderer' => 'DifferentialChangesetRenderer',
     'DifferentialChangesetTwoUpRenderer' => 'DifferentialChangesetHTMLRenderer',
     'DifferentialChangesetTwoUpTestRenderer' => 'DifferentialChangesetTestRenderer',
     'DifferentialChangesetViewController' => 'DifferentialController',
     'DifferentialCloseConduitAPIMethod' => 'DifferentialConduitAPIMethod',
     'DifferentialCommentPreviewController' => 'DifferentialController',
     'DifferentialCommentSaveController' => 'DifferentialController',
+    'DifferentialCommitMessageParser' => 'Phobject',
     'DifferentialCommitMessageParserTestCase' => 'PhabricatorTestCase',
     'DifferentialCommitsField' => 'DifferentialCustomField',
     'DifferentialConduitAPIMethod' => 'ConduitAPIMethod',
     'DifferentialConflictsField' => 'DifferentialCustomField',
     'DifferentialController' => 'PhabricatorController',
     'DifferentialCoreCustomField' => 'DifferentialCustomField',
     'DifferentialCreateCommentConduitAPIMethod' => 'DifferentialConduitAPIMethod',
     'DifferentialCreateDiffConduitAPIMethod' => 'DifferentialConduitAPIMethod',
     'DifferentialCreateInlineConduitAPIMethod' => 'DifferentialConduitAPIMethod',
     'DifferentialCreateMailReceiver' => 'PhabricatorMailReceiver',
     'DifferentialCreateRawDiffConduitAPIMethod' => 'DifferentialConduitAPIMethod',
     'DifferentialCreateRevisionConduitAPIMethod' => 'DifferentialConduitAPIMethod',
     'DifferentialCustomField' => 'PhabricatorCustomField',
     'DifferentialCustomFieldDependsOnParser' => 'PhabricatorCustomFieldMonogramParser',
     'DifferentialCustomFieldDependsOnParserTestCase' => 'PhabricatorTestCase',
     'DifferentialCustomFieldNumericIndex' => 'PhabricatorCustomFieldNumericIndexStorage',
     'DifferentialCustomFieldRevertsParser' => 'PhabricatorCustomFieldMonogramParser',
     'DifferentialCustomFieldRevertsParserTestCase' => 'PhabricatorTestCase',
     'DifferentialCustomFieldStorage' => 'PhabricatorCustomFieldStorage',
     'DifferentialCustomFieldStringIndex' => 'PhabricatorCustomFieldStringIndexStorage',
     'DifferentialDAO' => 'PhabricatorLiskDAO',
     'DifferentialDefaultViewCapability' => 'PhabricatorPolicyCapability',
     'DifferentialDependenciesField' => 'DifferentialCustomField',
     'DifferentialDependsOnField' => 'DifferentialCustomField',
     'DifferentialDiff' => array(
       'DifferentialDAO',
       'PhabricatorPolicyInterface',
       'HarbormasterBuildableInterface',
       'PhabricatorApplicationTransactionInterface',
       'PhabricatorDestructibleInterface',
     ),
     'DifferentialDiffCreateController' => 'DifferentialController',
     'DifferentialDiffEditor' => 'PhabricatorApplicationTransactionEditor',
     'DifferentialDiffInlineCommentQuery' => 'PhabricatorDiffInlineCommentQuery',
     'DifferentialDiffPHIDType' => 'PhabricatorPHIDType',
     'DifferentialDiffProperty' => 'DifferentialDAO',
     'DifferentialDiffQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'DifferentialDiffTableOfContentsView' => 'AphrontView',
     'DifferentialDiffTestCase' => 'PhutilTestCase',
     'DifferentialDiffTransaction' => 'PhabricatorApplicationTransaction',
     'DifferentialDiffViewController' => 'DifferentialController',
     'DifferentialDoorkeeperRevisionFeedStoryPublisher' => 'DoorkeeperFeedStoryPublisher',
     'DifferentialDraft' => 'DifferentialDAO',
     'DifferentialEditPolicyField' => 'DifferentialCoreCustomField',
     'DifferentialFieldParseException' => 'Exception',
     'DifferentialFieldValidationException' => 'Exception',
     'DifferentialFindConduitAPIMethod' => 'DifferentialConduitAPIMethod',
     'DifferentialFinishPostponedLintersConduitAPIMethod' => 'DifferentialConduitAPIMethod',
     'DifferentialGetAllDiffsConduitAPIMethod' => 'DifferentialConduitAPIMethod',
     'DifferentialGetCommitMessageConduitAPIMethod' => 'DifferentialConduitAPIMethod',
     'DifferentialGetCommitPathsConduitAPIMethod' => 'DifferentialConduitAPIMethod',
     'DifferentialGetDiffConduitAPIMethod' => 'DifferentialConduitAPIMethod',
     'DifferentialGetRawDiffConduitAPIMethod' => 'DifferentialConduitAPIMethod',
     'DifferentialGetRevisionCommentsConduitAPIMethod' => 'DifferentialConduitAPIMethod',
     'DifferentialGetRevisionConduitAPIMethod' => 'DifferentialConduitAPIMethod',
+    'DifferentialGetWorkingCopy' => 'Phobject',
     'DifferentialGitHubLandingStrategy' => 'DifferentialLandingStrategy',
     'DifferentialGitSVNIDField' => 'DifferentialCustomField',
     'DifferentialHiddenComment' => 'DifferentialDAO',
     'DifferentialHostField' => 'DifferentialCustomField',
     'DifferentialHostedGitLandingStrategy' => 'DifferentialLandingStrategy',
     'DifferentialHostedMercurialLandingStrategy' => 'DifferentialLandingStrategy',
     'DifferentialHovercardEventListener' => 'PhabricatorEventListener',
     'DifferentialHunk' => array(
       'DifferentialDAO',
       'PhabricatorPolicyInterface',
     ),
+    'DifferentialHunkParser' => 'Phobject',
     'DifferentialHunkParserTestCase' => 'PhabricatorTestCase',
     'DifferentialHunkQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'DifferentialHunkTestCase' => 'PhutilTestCase',
-    'DifferentialInlineComment' => 'PhabricatorInlineCommentInterface',
+    'DifferentialInlineComment' => array(
+      'Phobject',
+      'PhabricatorInlineCommentInterface',
+    ),
     'DifferentialInlineCommentEditController' => 'PhabricatorInlineCommentController',
     'DifferentialInlineCommentPreviewController' => 'PhabricatorInlineCommentPreviewController',
     'DifferentialInlineCommentQuery' => 'PhabricatorOffsetPagedQuery',
     'DifferentialJIRAIssuesField' => 'DifferentialStoredCustomField',
     'DifferentialLandingActionMenuEventListener' => 'PhabricatorEventListener',
+    'DifferentialLandingStrategy' => 'Phobject',
     'DifferentialLegacyHunk' => 'DifferentialHunk',
     'DifferentialLineAdjustmentMap' => 'Phobject',
     'DifferentialLintField' => 'DifferentialCustomField',
+    'DifferentialLintStatus' => 'Phobject',
     'DifferentialLocalCommitsView' => 'AphrontView',
     'DifferentialManiphestTasksField' => 'DifferentialCoreCustomField',
     'DifferentialModernHunk' => 'DifferentialHunk',
     'DifferentialParseCacheGarbageCollector' => 'PhabricatorGarbageCollector',
     'DifferentialParseCommitMessageConduitAPIMethod' => 'DifferentialConduitAPIMethod',
     'DifferentialParseRenderTestCase' => 'PhabricatorTestCase',
     'DifferentialPathField' => 'DifferentialCustomField',
     'DifferentialPrimaryPaneView' => 'AphrontView',
     'DifferentialProjectReviewersField' => 'DifferentialCustomField',
     'DifferentialProjectsField' => 'DifferentialCoreCustomField',
     'DifferentialQueryConduitAPIMethod' => 'DifferentialConduitAPIMethod',
     'DifferentialQueryDiffsConduitAPIMethod' => 'DifferentialConduitAPIMethod',
+    'DifferentialRawDiffRenderer' => 'Phobject',
+    'DifferentialReleephRequestFieldSpecification' => 'Phobject',
     'DifferentialRemarkupRule' => 'PhabricatorObjectRemarkupRule',
     'DifferentialReplyHandler' => 'PhabricatorApplicationTransactionReplyHandler',
     'DifferentialRepositoryField' => 'DifferentialCoreCustomField',
     'DifferentialRepositoryLookup' => 'Phobject',
     'DifferentialRequiredSignaturesField' => 'DifferentialCoreCustomField',
     'DifferentialResultsTableView' => 'AphrontView',
     'DifferentialRevertPlanField' => 'DifferentialStoredCustomField',
     'DifferentialReviewedByField' => 'DifferentialCoreCustomField',
+    'DifferentialReviewer' => 'Phobject',
     'DifferentialReviewerForRevisionEdgeType' => 'PhabricatorEdgeType',
+    'DifferentialReviewerStatus' => 'Phobject',
     'DifferentialReviewersField' => 'DifferentialCoreCustomField',
     'DifferentialReviewersView' => 'AphrontView',
     'DifferentialRevision' => array(
       'DifferentialDAO',
       'PhabricatorTokenReceiverInterface',
       'PhabricatorPolicyInterface',
       'PhabricatorExtendedPolicyInterface',
       'PhabricatorFlaggableInterface',
       'PhrequentTrackableInterface',
       'HarbormasterBuildableInterface',
       'PhabricatorSubscribableInterface',
       'PhabricatorCustomFieldInterface',
       'PhabricatorApplicationTransactionInterface',
       'PhabricatorMentionableInterface',
       'PhabricatorDestructibleInterface',
       'PhabricatorProjectInterface',
     ),
     'DifferentialRevisionCloseDetailsController' => 'DifferentialController',
+    'DifferentialRevisionControlSystem' => 'Phobject',
     'DifferentialRevisionDependedOnByRevisionEdgeType' => 'PhabricatorEdgeType',
     'DifferentialRevisionDependsOnRevisionEdgeType' => 'PhabricatorEdgeType',
     'DifferentialRevisionDetailView' => 'AphrontView',
     'DifferentialRevisionEditController' => 'DifferentialController',
     'DifferentialRevisionHasCommitEdgeType' => 'PhabricatorEdgeType',
     'DifferentialRevisionHasReviewerEdgeType' => 'PhabricatorEdgeType',
     'DifferentialRevisionHasTaskEdgeType' => 'PhabricatorEdgeType',
     'DifferentialRevisionIDField' => 'DifferentialCustomField',
     'DifferentialRevisionLandController' => 'DifferentialController',
     'DifferentialRevisionListController' => 'DifferentialController',
     'DifferentialRevisionListView' => 'AphrontView',
     'DifferentialRevisionMailReceiver' => 'PhabricatorObjectMailReceiver',
     'DifferentialRevisionPHIDType' => 'PhabricatorPHIDType',
     'DifferentialRevisionQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'DifferentialRevisionSearchEngine' => 'PhabricatorApplicationSearchEngine',
+    'DifferentialRevisionStatus' => 'Phobject',
     'DifferentialRevisionUpdateHistoryView' => 'AphrontView',
     'DifferentialRevisionViewController' => 'DifferentialController',
     'DifferentialSchemaSpec' => 'PhabricatorConfigSchemaSpec',
     'DifferentialSearchIndexer' => 'PhabricatorSearchDocumentIndexer',
     'DifferentialSetDiffPropertyConduitAPIMethod' => 'DifferentialConduitAPIMethod',
     'DifferentialStoredCustomField' => 'DifferentialCustomField',
     'DifferentialSubscribersField' => 'DifferentialCoreCustomField',
     'DifferentialSummaryField' => 'DifferentialCoreCustomField',
     'DifferentialTestPlanField' => 'DifferentialCoreCustomField',
     'DifferentialTitleField' => 'DifferentialCoreCustomField',
     'DifferentialTransaction' => 'PhabricatorApplicationTransaction',
     'DifferentialTransactionComment' => 'PhabricatorApplicationTransactionComment',
     'DifferentialTransactionEditor' => 'PhabricatorApplicationTransactionEditor',
     'DifferentialTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
     'DifferentialTransactionView' => 'PhabricatorApplicationTransactionView',
     'DifferentialUnitField' => 'DifferentialCustomField',
+    'DifferentialUnitStatus' => 'Phobject',
+    'DifferentialUnitTestResult' => 'Phobject',
     'DifferentialUpdateRevisionConduitAPIMethod' => 'DifferentialConduitAPIMethod',
     'DifferentialUpdateUnitResultsConduitAPIMethod' => 'DifferentialConduitAPIMethod',
     'DifferentialViewPolicyField' => 'DifferentialCoreCustomField',
     'DiffusionAuditorDatasource' => 'PhabricatorTypeaheadCompositeDatasource',
     'DiffusionBranchQueryConduitAPIMethod' => 'DiffusionQueryConduitAPIMethod',
     'DiffusionBranchTableController' => 'DiffusionController',
     'DiffusionBranchTableView' => 'DiffusionView',
     'DiffusionBrowseController' => 'DiffusionController',
     'DiffusionBrowseDirectoryController' => 'DiffusionBrowseController',
     'DiffusionBrowseFileController' => 'DiffusionBrowseController',
     'DiffusionBrowseMainController' => 'DiffusionBrowseController',
     'DiffusionBrowseQueryConduitAPIMethod' => 'DiffusionQueryConduitAPIMethod',
+    'DiffusionBrowseResultSet' => 'Phobject',
     'DiffusionBrowseSearchController' => 'DiffusionBrowseController',
     'DiffusionBrowseTableView' => 'DiffusionView',
     'DiffusionCachedResolveRefsQuery' => 'DiffusionLowLevelQuery',
     'DiffusionChangeController' => 'DiffusionController',
     'DiffusionCommitBranchesController' => 'DiffusionController',
     'DiffusionCommitChangeTableView' => 'DiffusionView',
     'DiffusionCommitController' => 'DiffusionController',
     'DiffusionCommitEditController' => 'DiffusionController',
     'DiffusionCommitHasRevisionEdgeType' => 'PhabricatorEdgeType',
     'DiffusionCommitHasTaskEdgeType' => 'PhabricatorEdgeType',
     'DiffusionCommitHash' => 'Phobject',
     'DiffusionCommitHookEngine' => 'Phobject',
     'DiffusionCommitHookRejectException' => 'Exception',
     'DiffusionCommitParentsQueryConduitAPIMethod' => 'DiffusionQueryConduitAPIMethod',
     'DiffusionCommitQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'DiffusionCommitRef' => 'Phobject',
     'DiffusionCommitRemarkupRule' => 'PhabricatorObjectRemarkupRule',
     'DiffusionCommitRemarkupRuleTestCase' => 'PhabricatorTestCase',
     'DiffusionCommitRevertedByCommitEdgeType' => 'PhabricatorEdgeType',
     'DiffusionCommitRevertsCommitEdgeType' => 'PhabricatorEdgeType',
     'DiffusionCommitTagsController' => 'DiffusionController',
     'DiffusionConduitAPIMethod' => 'ConduitAPIMethod',
     'DiffusionController' => 'PhabricatorController',
     'DiffusionCreateCommentConduitAPIMethod' => 'DiffusionConduitAPIMethod',
     'DiffusionCreateRepositoriesCapability' => 'PhabricatorPolicyCapability',
     'DiffusionDefaultEditCapability' => 'PhabricatorPolicyCapability',
     'DiffusionDefaultPushCapability' => 'PhabricatorPolicyCapability',
     'DiffusionDefaultViewCapability' => 'PhabricatorPolicyCapability',
     'DiffusionDiffController' => 'DiffusionController',
     'DiffusionDiffInlineCommentQuery' => 'PhabricatorDiffInlineCommentQuery',
     'DiffusionDiffQueryConduitAPIMethod' => 'DiffusionQueryConduitAPIMethod',
     'DiffusionDoorkeeperCommitFeedStoryPublisher' => 'DoorkeeperFeedStoryPublisher',
     'DiffusionEmptyResultView' => 'DiffusionView',
     'DiffusionExistsQueryConduitAPIMethod' => 'DiffusionQueryConduitAPIMethod',
     'DiffusionExternalController' => 'DiffusionController',
+    'DiffusionExternalSymbolQuery' => 'Phobject',
+    'DiffusionExternalSymbolsSource' => 'Phobject',
+    'DiffusionFileContent' => 'Phobject',
     'DiffusionFileContentQuery' => 'DiffusionQuery',
     'DiffusionFileContentQueryConduitAPIMethod' => 'DiffusionQueryConduitAPIMethod',
     'DiffusionFindSymbolsConduitAPIMethod' => 'DiffusionConduitAPIMethod',
     'DiffusionGetCommitsConduitAPIMethod' => 'DiffusionConduitAPIMethod',
     'DiffusionGetLintMessagesConduitAPIMethod' => 'DiffusionConduitAPIMethod',
     'DiffusionGetRecentCommitsByPathConduitAPIMethod' => 'DiffusionConduitAPIMethod',
+    'DiffusionGitBranch' => 'Phobject',
     'DiffusionGitBranchTestCase' => 'PhabricatorTestCase',
     'DiffusionGitFileContentQuery' => 'DiffusionFileContentQuery',
     'DiffusionGitFileContentQueryTestCase' => 'PhabricatorTestCase',
     'DiffusionGitRawDiffQuery' => 'DiffusionRawDiffQuery',
     'DiffusionGitReceivePackSSHWorkflow' => 'DiffusionGitSSHWorkflow',
     'DiffusionGitRequest' => 'DiffusionRequest',
     'DiffusionGitResponse' => 'AphrontResponse',
     'DiffusionGitSSHWorkflow' => 'DiffusionSSHWorkflow',
     'DiffusionGitUploadPackSSHWorkflow' => 'DiffusionGitSSHWorkflow',
     'DiffusionHistoryController' => 'DiffusionController',
     'DiffusionHistoryQueryConduitAPIMethod' => 'DiffusionQueryConduitAPIMethod',
     'DiffusionHistoryTableView' => 'DiffusionView',
     'DiffusionHovercardEventListener' => 'PhabricatorEventListener',
     'DiffusionInlineCommentController' => 'PhabricatorInlineCommentController',
     'DiffusionInlineCommentPreviewController' => 'PhabricatorInlineCommentPreviewController',
     'DiffusionLastModifiedController' => 'DiffusionController',
     'DiffusionLastModifiedQueryConduitAPIMethod' => 'DiffusionQueryConduitAPIMethod',
     'DiffusionLintController' => 'DiffusionController',
     'DiffusionLintCountQuery' => 'PhabricatorQuery',
     'DiffusionLintDetailsController' => 'DiffusionController',
+    'DiffusionLintSaveRunner' => 'Phobject',
     'DiffusionLookSoonConduitAPIMethod' => 'DiffusionConduitAPIMethod',
     'DiffusionLowLevelCommitFieldsQuery' => 'DiffusionLowLevelQuery',
     'DiffusionLowLevelCommitQuery' => 'DiffusionLowLevelQuery',
     'DiffusionLowLevelGitRefQuery' => 'DiffusionLowLevelQuery',
     'DiffusionLowLevelMercurialBranchesQuery' => 'DiffusionLowLevelQuery',
     'DiffusionLowLevelMercurialPathsQuery' => 'DiffusionLowLevelQuery',
     'DiffusionLowLevelParentsQuery' => 'DiffusionLowLevelQuery',
     'DiffusionLowLevelQuery' => 'Phobject',
     'DiffusionLowLevelResolveRefsQuery' => 'DiffusionLowLevelQuery',
     'DiffusionMercurialFileContentQuery' => 'DiffusionFileContentQuery',
     'DiffusionMercurialRawDiffQuery' => 'DiffusionRawDiffQuery',
     'DiffusionMercurialRequest' => 'DiffusionRequest',
     'DiffusionMercurialResponse' => 'AphrontResponse',
     'DiffusionMercurialSSHWorkflow' => 'DiffusionSSHWorkflow',
     'DiffusionMercurialServeSSHWorkflow' => 'DiffusionMercurialSSHWorkflow',
     'DiffusionMercurialWireClientSSHProtocolChannel' => 'PhutilProtocolChannel',
+    'DiffusionMercurialWireProtocol' => 'Phobject',
     'DiffusionMercurialWireSSHTestCase' => 'PhabricatorTestCase',
     'DiffusionMergedCommitsQueryConduitAPIMethod' => 'DiffusionQueryConduitAPIMethod',
     'DiffusionMirrorDeleteController' => 'DiffusionController',
     'DiffusionMirrorEditController' => 'DiffusionController',
+    'DiffusionPathChange' => 'Phobject',
+    'DiffusionPathChangeQuery' => 'Phobject',
     'DiffusionPathCompleteController' => 'DiffusionController',
+    'DiffusionPathIDQuery' => 'Phobject',
+    'DiffusionPathQuery' => 'Phobject',
     'DiffusionPathQueryTestCase' => 'PhabricatorTestCase',
     'DiffusionPathTreeController' => 'DiffusionController',
     'DiffusionPathValidateController' => 'DiffusionController',
     'DiffusionPhpExternalSymbolsSource' => 'DiffusionExternalSymbolsSource',
     'DiffusionPushCapability' => 'PhabricatorPolicyCapability',
     'DiffusionPushEventViewController' => 'DiffusionPushLogController',
     'DiffusionPushLogController' => 'DiffusionController',
     'DiffusionPushLogListController' => 'DiffusionPushLogController',
     'DiffusionPushLogListView' => 'AphrontView',
     'DiffusionPythonExternalSymbolsSource' => 'DiffusionExternalSymbolsSource',
     'DiffusionQuery' => 'PhabricatorQuery',
     'DiffusionQueryCommitsConduitAPIMethod' => 'DiffusionConduitAPIMethod',
     'DiffusionQueryConduitAPIMethod' => 'DiffusionConduitAPIMethod',
     'DiffusionQueryPathsConduitAPIMethod' => 'DiffusionQueryConduitAPIMethod',
     'DiffusionRawDiffQuery' => 'DiffusionQuery',
     'DiffusionRawDiffQueryConduitAPIMethod' => 'DiffusionQueryConduitAPIMethod',
     'DiffusionReadmeView' => 'DiffusionView',
     'DiffusionRefNotFoundException' => 'Exception',
     'DiffusionRefTableController' => 'DiffusionController',
     'DiffusionRefsQueryConduitAPIMethod' => 'DiffusionQueryConduitAPIMethod',
+    'DiffusionRenameHistoryQuery' => 'Phobject',
     'DiffusionRepositoryByIDRemarkupRule' => 'PhabricatorObjectRemarkupRule',
     'DiffusionRepositoryController' => 'DiffusionController',
     'DiffusionRepositoryCreateController' => 'DiffusionRepositoryEditController',
     'DiffusionRepositoryDatasource' => 'PhabricatorTypeaheadDatasource',
     'DiffusionRepositoryDefaultController' => 'DiffusionController',
     'DiffusionRepositoryEditActionsController' => 'DiffusionRepositoryEditController',
     'DiffusionRepositoryEditActivateController' => 'DiffusionRepositoryEditController',
     'DiffusionRepositoryEditBasicController' => 'DiffusionRepositoryEditController',
     'DiffusionRepositoryEditBranchesController' => 'DiffusionRepositoryEditController',
     'DiffusionRepositoryEditController' => 'DiffusionController',
     'DiffusionRepositoryEditDangerousController' => 'DiffusionRepositoryEditController',
     'DiffusionRepositoryEditDeleteController' => 'DiffusionRepositoryEditController',
     'DiffusionRepositoryEditEncodingController' => 'DiffusionRepositoryEditController',
     'DiffusionRepositoryEditHostingController' => 'DiffusionRepositoryEditController',
     'DiffusionRepositoryEditMainController' => 'DiffusionRepositoryEditController',
     'DiffusionRepositoryEditStagingController' => 'DiffusionRepositoryEditController',
     'DiffusionRepositoryEditStorageController' => 'DiffusionRepositoryEditController',
     'DiffusionRepositoryEditSubversionController' => 'DiffusionRepositoryEditController',
     'DiffusionRepositoryEditUpdateController' => 'DiffusionRepositoryEditController',
     'DiffusionRepositoryListController' => 'DiffusionController',
     'DiffusionRepositoryNewController' => 'DiffusionController',
+    'DiffusionRepositoryPath' => 'Phobject',
     'DiffusionRepositoryRef' => 'Phobject',
     'DiffusionRepositoryRemarkupRule' => 'PhabricatorObjectRemarkupRule',
     'DiffusionRepositorySymbolsController' => 'DiffusionRepositoryEditController',
+    'DiffusionRepositoryTag' => 'Phobject',
+    'DiffusionRequest' => 'Phobject',
     'DiffusionResolveRefsConduitAPIMethod' => 'DiffusionQueryConduitAPIMethod',
     'DiffusionResolveUserQuery' => 'Phobject',
     'DiffusionSSHWorkflow' => 'PhabricatorSSHWorkflow',
     'DiffusionSearchQueryConduitAPIMethod' => 'DiffusionQueryConduitAPIMethod',
     'DiffusionServeController' => 'DiffusionController',
     'DiffusionSetPasswordSettingsPanel' => 'PhabricatorSettingsPanel',
     'DiffusionSetupException' => 'AphrontUsageException',
     'DiffusionSubversionSSHWorkflow' => 'DiffusionSSHWorkflow',
     'DiffusionSubversionServeSSHWorkflow' => 'DiffusionSubversionSSHWorkflow',
     'DiffusionSubversionWireProtocol' => 'Phobject',
     'DiffusionSubversionWireProtocolTestCase' => 'PhabricatorTestCase',
     'DiffusionSvnFileContentQuery' => 'DiffusionFileContentQuery',
     'DiffusionSvnRawDiffQuery' => 'DiffusionRawDiffQuery',
     'DiffusionSvnRequest' => 'DiffusionRequest',
     'DiffusionSymbolController' => 'DiffusionController',
     'DiffusionSymbolDatasource' => 'PhabricatorTypeaheadDatasource',
     'DiffusionSymbolQuery' => 'PhabricatorOffsetPagedQuery',
     'DiffusionTagListController' => 'DiffusionController',
     'DiffusionTagListView' => 'DiffusionView',
     'DiffusionTagsQueryConduitAPIMethod' => 'DiffusionQueryConduitAPIMethod',
     'DiffusionURITestCase' => 'PhutilTestCase',
     'DiffusionUpdateCoverageConduitAPIMethod' => 'DiffusionConduitAPIMethod',
     'DiffusionView' => 'AphrontView',
     'DivinerArticleAtomizer' => 'DivinerAtomizer',
+    'DivinerAtom' => 'Phobject',
     'DivinerAtomCache' => 'DivinerDiskCache',
     'DivinerAtomController' => 'DivinerController',
     'DivinerAtomListController' => 'DivinerController',
     'DivinerAtomPHIDType' => 'PhabricatorPHIDType',
     'DivinerAtomQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
+    'DivinerAtomRef' => 'Phobject',
     'DivinerAtomSearchEngine' => 'PhabricatorApplicationSearchEngine',
     'DivinerAtomSearchIndexer' => 'PhabricatorSearchDocumentIndexer',
     'DivinerAtomizeWorkflow' => 'DivinerWorkflow',
+    'DivinerAtomizer' => 'Phobject',
     'DivinerBookController' => 'DivinerController',
     'DivinerBookItemView' => 'AphrontTagView',
     'DivinerBookPHIDType' => 'PhabricatorPHIDType',
     'DivinerBookQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'DivinerBookSearchIndexer' => 'PhabricatorSearchDocumentIndexer',
     'DivinerController' => 'PhabricatorController',
     'DivinerDAO' => 'PhabricatorLiskDAO',
     'DivinerDefaultRenderer' => 'DivinerRenderer',
+    'DivinerDiskCache' => 'Phobject',
     'DivinerFileAtomizer' => 'DivinerAtomizer',
     'DivinerFindController' => 'DivinerController',
     'DivinerGenerateWorkflow' => 'DivinerWorkflow',
     'DivinerLiveAtom' => 'DivinerDAO',
     'DivinerLiveBook' => array(
       'DivinerDAO',
       'PhabricatorPolicyInterface',
       'PhabricatorDestructibleInterface',
     ),
     'DivinerLivePublisher' => 'DivinerPublisher',
     'DivinerLiveSymbol' => array(
       'DivinerDAO',
       'PhabricatorPolicyInterface',
       'PhabricatorMarkupInterface',
       'PhabricatorDestructibleInterface',
     ),
     'DivinerMainController' => 'DivinerController',
     'DivinerPHPAtomizer' => 'DivinerAtomizer',
     'DivinerParameterTableView' => 'AphrontTagView',
     'DivinerPublishCache' => 'DivinerDiskCache',
+    'DivinerPublisher' => 'Phobject',
+    'DivinerRenderer' => 'Phobject',
     'DivinerReturnTableView' => 'AphrontTagView',
     'DivinerSectionView' => 'AphrontTagView',
     'DivinerStaticPublisher' => 'DivinerPublisher',
     'DivinerSymbolRemarkupRule' => 'PhutilRemarkupRule',
     'DivinerWorkflow' => 'PhabricatorManagementWorkflow',
     'DoorkeeperAsanaFeedWorker' => 'DoorkeeperFeedWorker',
     'DoorkeeperAsanaRemarkupRule' => 'DoorkeeperRemarkupRule',
     'DoorkeeperBridge' => 'Phobject',
     'DoorkeeperBridgeAsana' => 'DoorkeeperBridge',
     'DoorkeeperBridgeJIRA' => 'DoorkeeperBridge',
     'DoorkeeperBridgeJIRATestCase' => 'PhabricatorTestCase',
     'DoorkeeperDAO' => 'PhabricatorLiskDAO',
     'DoorkeeperExternalObject' => array(
       'DoorkeeperDAO',
       'PhabricatorPolicyInterface',
     ),
     'DoorkeeperExternalObjectQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
+    'DoorkeeperFeedStoryPublisher' => 'Phobject',
     'DoorkeeperFeedWorker' => 'FeedPushWorker',
     'DoorkeeperImportEngine' => 'Phobject',
     'DoorkeeperJIRAFeedWorker' => 'DoorkeeperFeedWorker',
     'DoorkeeperJIRARemarkupRule' => 'DoorkeeperRemarkupRule',
     'DoorkeeperMissingLinkException' => 'Exception',
     'DoorkeeperObjectRef' => 'Phobject',
     'DoorkeeperRemarkupRule' => 'PhutilRemarkupRule',
     'DoorkeeperSchemaSpec' => 'PhabricatorConfigSchemaSpec',
     'DoorkeeperTagView' => 'AphrontView',
     'DoorkeeperTagsController' => 'PhabricatorController',
     'DrydockAllocatorWorker' => 'PhabricatorWorker',
     'DrydockApacheWebrootInterface' => 'DrydockWebrootInterface',
     'DrydockBlueprint' => array(
       'DrydockDAO',
       'PhabricatorApplicationTransactionInterface',
       'PhabricatorPolicyInterface',
       'PhabricatorCustomFieldInterface',
     ),
     'DrydockBlueprintController' => 'DrydockController',
     'DrydockBlueprintCoreCustomField' => array(
       'DrydockBlueprintCustomField',
       'PhabricatorStandardCustomFieldInterface',
     ),
     'DrydockBlueprintCreateController' => 'DrydockBlueprintController',
     'DrydockBlueprintCustomField' => 'PhabricatorCustomField',
     'DrydockBlueprintEditController' => 'DrydockBlueprintController',
     'DrydockBlueprintEditor' => 'PhabricatorApplicationTransactionEditor',
+    'DrydockBlueprintImplementation' => 'Phobject',
+    'DrydockBlueprintImplementationTestCase' => 'PhabricatorTestCase',
     'DrydockBlueprintListController' => 'DrydockBlueprintController',
     'DrydockBlueprintPHIDType' => 'PhabricatorPHIDType',
     'DrydockBlueprintQuery' => 'DrydockQuery',
+    'DrydockBlueprintScopeGuard' => 'Phobject',
     'DrydockBlueprintSearchEngine' => 'PhabricatorApplicationSearchEngine',
     'DrydockBlueprintTransaction' => 'PhabricatorApplicationTransaction',
     'DrydockBlueprintTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
     'DrydockBlueprintViewController' => 'DrydockBlueprintController',
     'DrydockCommandInterface' => 'DrydockInterface',
     'DrydockConsoleController' => 'DrydockController',
+    'DrydockConstants' => 'Phobject',
     'DrydockController' => 'PhabricatorController',
     'DrydockCreateBlueprintsCapability' => 'PhabricatorPolicyCapability',
     'DrydockDAO' => 'PhabricatorLiskDAO',
     'DrydockDefaultEditCapability' => 'PhabricatorPolicyCapability',
     'DrydockDefaultViewCapability' => 'PhabricatorPolicyCapability',
     'DrydockFilesystemInterface' => 'DrydockInterface',
+    'DrydockInterface' => 'Phobject',
     'DrydockLease' => array(
       'DrydockDAO',
       'PhabricatorPolicyInterface',
     ),
     'DrydockLeaseController' => 'DrydockController',
     'DrydockLeaseListController' => 'DrydockLeaseController',
     'DrydockLeaseListView' => 'AphrontView',
     'DrydockLeasePHIDType' => 'PhabricatorPHIDType',
     'DrydockLeaseQuery' => 'DrydockQuery',
     'DrydockLeaseReleaseController' => 'DrydockLeaseController',
     'DrydockLeaseSearchEngine' => 'PhabricatorApplicationSearchEngine',
     'DrydockLeaseStatus' => 'DrydockConstants',
     'DrydockLeaseViewController' => 'DrydockLeaseController',
     'DrydockLocalCommandInterface' => 'DrydockCommandInterface',
     'DrydockLog' => array(
       'DrydockDAO',
       'PhabricatorPolicyInterface',
     ),
     'DrydockLogController' => 'DrydockController',
     'DrydockLogListController' => 'DrydockLogController',
     'DrydockLogListView' => 'AphrontView',
     'DrydockLogQuery' => 'DrydockQuery',
     'DrydockLogSearchEngine' => 'PhabricatorApplicationSearchEngine',
     'DrydockManagementCloseWorkflow' => 'DrydockManagementWorkflow',
     'DrydockManagementCreateResourceWorkflow' => 'DrydockManagementWorkflow',
     'DrydockManagementLeaseWorkflow' => 'DrydockManagementWorkflow',
     'DrydockManagementReleaseWorkflow' => 'DrydockManagementWorkflow',
     'DrydockManagementWorkflow' => 'PhabricatorManagementWorkflow',
     'DrydockPreallocatedHostBlueprintImplementation' => 'DrydockBlueprintImplementation',
     'DrydockQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'DrydockResource' => array(
       'DrydockDAO',
       'PhabricatorPolicyInterface',
     ),
     'DrydockResourceCloseController' => 'DrydockResourceController',
     'DrydockResourceController' => 'DrydockController',
     'DrydockResourceListController' => 'DrydockResourceController',
     'DrydockResourceListView' => 'AphrontView',
     'DrydockResourcePHIDType' => 'PhabricatorPHIDType',
     'DrydockResourceQuery' => 'DrydockQuery',
     'DrydockResourceSearchEngine' => 'PhabricatorApplicationSearchEngine',
     'DrydockResourceStatus' => 'DrydockConstants',
     'DrydockResourceViewController' => 'DrydockResourceController',
     'DrydockSFTPFilesystemInterface' => 'DrydockFilesystemInterface',
     'DrydockSSHCommandInterface' => 'DrydockCommandInterface',
     'DrydockWebrootInterface' => 'DrydockInterface',
     'DrydockWorkingCopyBlueprintImplementation' => 'DrydockBlueprintImplementation',
     'FeedConduitAPIMethod' => 'ConduitAPIMethod',
     'FeedPublishConduitAPIMethod' => 'FeedConduitAPIMethod',
     'FeedPublisherHTTPWorker' => 'FeedPushWorker',
     'FeedPublisherWorker' => 'FeedPushWorker',
     'FeedPushWorker' => 'PhabricatorWorker',
     'FeedQueryConduitAPIMethod' => 'FeedConduitAPIMethod',
+    'FeedStoryNotificationGarbageCollector' => 'PhabricatorGarbageCollector',
     'FileAllocateConduitAPIMethod' => 'FileConduitAPIMethod',
     'FileConduitAPIMethod' => 'ConduitAPIMethod',
     'FileCreateMailReceiver' => 'PhabricatorMailReceiver',
     'FileDownloadConduitAPIMethod' => 'FileConduitAPIMethod',
     'FileInfoConduitAPIMethod' => 'FileConduitAPIMethod',
     'FileMailReceiver' => 'PhabricatorObjectMailReceiver',
     'FileQueryChunksConduitAPIMethod' => 'FileConduitAPIMethod',
     'FileReplyHandler' => 'PhabricatorApplicationTransactionReplyHandler',
     'FileUploadChunkConduitAPIMethod' => 'FileConduitAPIMethod',
     'FileUploadConduitAPIMethod' => 'FileConduitAPIMethod',
     'FileUploadHashConduitAPIMethod' => 'FileConduitAPIMethod',
     'FilesDefaultViewCapability' => 'PhabricatorPolicyCapability',
     'FlagConduitAPIMethod' => 'ConduitAPIMethod',
     'FlagDeleteConduitAPIMethod' => 'FlagConduitAPIMethod',
     'FlagEditConduitAPIMethod' => 'FlagConduitAPIMethod',
     'FlagQueryConduitAPIMethod' => 'FlagConduitAPIMethod',
     'FundBacker' => array(
       'FundDAO',
       'PhabricatorPolicyInterface',
       'PhabricatorApplicationTransactionInterface',
     ),
     'FundBackerCart' => 'PhortuneCartImplementation',
     'FundBackerEditor' => 'PhabricatorApplicationTransactionEditor',
     'FundBackerListController' => 'FundController',
     'FundBackerPHIDType' => 'PhabricatorPHIDType',
     'FundBackerProduct' => 'PhortuneProductImplementation',
     'FundBackerQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'FundBackerSearchEngine' => 'PhabricatorApplicationSearchEngine',
     'FundBackerTransaction' => 'PhabricatorApplicationTransaction',
     'FundBackerTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
     'FundController' => 'PhabricatorController',
     'FundCreateInitiativesCapability' => 'PhabricatorPolicyCapability',
     'FundDAO' => 'PhabricatorLiskDAO',
     'FundDefaultViewCapability' => 'PhabricatorPolicyCapability',
     'FundInitiative' => array(
       'FundDAO',
       'PhabricatorPolicyInterface',
       'PhabricatorProjectInterface',
       'PhabricatorApplicationTransactionInterface',
       'PhabricatorSubscribableInterface',
       'PhabricatorMentionableInterface',
       'PhabricatorFlaggableInterface',
       'PhabricatorTokenReceiverInterface',
       'PhabricatorDestructibleInterface',
     ),
     'FundInitiativeBackController' => 'FundController',
     'FundInitiativeCloseController' => 'FundController',
     'FundInitiativeEditController' => 'FundController',
     'FundInitiativeEditor' => 'PhabricatorApplicationTransactionEditor',
     'FundInitiativeIndexer' => 'PhabricatorSearchDocumentIndexer',
     'FundInitiativeListController' => 'FundController',
     'FundInitiativePHIDType' => 'PhabricatorPHIDType',
     'FundInitiativeQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'FundInitiativeRemarkupRule' => 'PhabricatorObjectRemarkupRule',
     'FundInitiativeReplyHandler' => 'PhabricatorApplicationTransactionReplyHandler',
     'FundInitiativeSearchEngine' => 'PhabricatorApplicationSearchEngine',
     'FundInitiativeTransaction' => 'PhabricatorApplicationTransaction',
     'FundInitiativeTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
     'FundInitiativeViewController' => 'FundController',
     'FundSchemaSpec' => 'PhabricatorConfigSchemaSpec',
     'HarbormasterBuild' => array(
       'HarbormasterDAO',
       'PhabricatorApplicationTransactionInterface',
       'PhabricatorPolicyInterface',
     ),
     'HarbormasterBuildAbortedException' => 'Exception',
     'HarbormasterBuildActionController' => 'HarbormasterController',
     'HarbormasterBuildArtifact' => array(
       'HarbormasterDAO',
       'PhabricatorPolicyInterface',
     ),
     'HarbormasterBuildArtifactQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'HarbormasterBuildCommand' => 'HarbormasterDAO',
     'HarbormasterBuildDependencyDatasource' => 'PhabricatorTypeaheadDatasource',
     'HarbormasterBuildEngine' => 'Phobject',
     'HarbormasterBuildFailureException' => 'Exception',
     'HarbormasterBuildGraph' => 'AbstractDirectedGraph',
     'HarbormasterBuildItem' => 'HarbormasterDAO',
     'HarbormasterBuildItemPHIDType' => 'PhabricatorPHIDType',
     'HarbormasterBuildItemQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'HarbormasterBuildLog' => array(
       'HarbormasterDAO',
       'PhabricatorPolicyInterface',
     ),
     'HarbormasterBuildLogPHIDType' => 'PhabricatorPHIDType',
     'HarbormasterBuildLogQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'HarbormasterBuildMessage' => array(
       'HarbormasterDAO',
       'PhabricatorPolicyInterface',
     ),
     'HarbormasterBuildMessageQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'HarbormasterBuildPHIDType' => 'PhabricatorPHIDType',
     'HarbormasterBuildPlan' => array(
       'HarbormasterDAO',
       'PhabricatorApplicationTransactionInterface',
       'PhabricatorPolicyInterface',
       'PhabricatorSubscribableInterface',
     ),
     'HarbormasterBuildPlanDatasource' => 'PhabricatorTypeaheadDatasource',
     'HarbormasterBuildPlanEditor' => 'PhabricatorApplicationTransactionEditor',
     'HarbormasterBuildPlanPHIDType' => 'PhabricatorPHIDType',
     'HarbormasterBuildPlanQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'HarbormasterBuildPlanSearchEngine' => 'PhabricatorApplicationSearchEngine',
     'HarbormasterBuildPlanTransaction' => 'PhabricatorApplicationTransaction',
     'HarbormasterBuildPlanTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
     'HarbormasterBuildQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'HarbormasterBuildStep' => array(
       'HarbormasterDAO',
       'PhabricatorApplicationTransactionInterface',
       'PhabricatorPolicyInterface',
       'PhabricatorCustomFieldInterface',
     ),
     'HarbormasterBuildStepCoreCustomField' => array(
       'HarbormasterBuildStepCustomField',
       'PhabricatorStandardCustomFieldInterface',
     ),
     'HarbormasterBuildStepCustomField' => 'PhabricatorCustomField',
     'HarbormasterBuildStepEditor' => 'PhabricatorApplicationTransactionEditor',
+    'HarbormasterBuildStepImplementation' => 'Phobject',
+    'HarbormasterBuildStepImplementationTestCase' => 'PhabricatorTestCase',
     'HarbormasterBuildStepPHIDType' => 'PhabricatorPHIDType',
     'HarbormasterBuildStepQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'HarbormasterBuildStepTransaction' => 'PhabricatorApplicationTransaction',
     'HarbormasterBuildStepTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
     'HarbormasterBuildTarget' => array(
       'HarbormasterDAO',
       'PhabricatorPolicyInterface',
     ),
     'HarbormasterBuildTargetPHIDType' => 'PhabricatorPHIDType',
     'HarbormasterBuildTargetQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'HarbormasterBuildTransaction' => 'PhabricatorApplicationTransaction',
     'HarbormasterBuildTransactionEditor' => 'PhabricatorApplicationTransactionEditor',
     'HarbormasterBuildTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
     'HarbormasterBuildViewController' => 'HarbormasterController',
     'HarbormasterBuildWorker' => 'HarbormasterWorker',
     'HarbormasterBuildable' => array(
       'HarbormasterDAO',
       'PhabricatorApplicationTransactionInterface',
       'PhabricatorPolicyInterface',
       'HarbormasterBuildableInterface',
     ),
     'HarbormasterBuildableActionController' => 'HarbormasterController',
     'HarbormasterBuildableListController' => 'HarbormasterController',
     'HarbormasterBuildablePHIDType' => 'PhabricatorPHIDType',
     'HarbormasterBuildableQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'HarbormasterBuildableSearchEngine' => 'PhabricatorApplicationSearchEngine',
     'HarbormasterBuildableTransaction' => 'PhabricatorApplicationTransaction',
     'HarbormasterBuildableTransactionEditor' => 'PhabricatorApplicationTransactionEditor',
     'HarbormasterBuildableTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
     'HarbormasterBuildableViewController' => 'HarbormasterController',
     'HarbormasterCommandBuildStepImplementation' => 'HarbormasterBuildStepImplementation',
     'HarbormasterConduitAPIMethod' => 'ConduitAPIMethod',
     'HarbormasterController' => 'PhabricatorController',
     'HarbormasterDAO' => 'PhabricatorLiskDAO',
     'HarbormasterHTTPRequestBuildStepImplementation' => 'HarbormasterBuildStepImplementation',
     'HarbormasterLeaseHostBuildStepImplementation' => 'HarbormasterBuildStepImplementation',
     'HarbormasterManagePlansCapability' => 'PhabricatorPolicyCapability',
     'HarbormasterManagementBuildWorkflow' => 'HarbormasterManagementWorkflow',
     'HarbormasterManagementUpdateWorkflow' => 'HarbormasterManagementWorkflow',
     'HarbormasterManagementWorkflow' => 'PhabricatorManagementWorkflow',
     'HarbormasterObject' => 'HarbormasterDAO',
     'HarbormasterPlanController' => 'HarbormasterController',
     'HarbormasterPlanDisableController' => 'HarbormasterPlanController',
     'HarbormasterPlanEditController' => 'HarbormasterPlanController',
     'HarbormasterPlanListController' => 'HarbormasterPlanController',
     'HarbormasterPlanRunController' => 'HarbormasterController',
     'HarbormasterPlanViewController' => 'HarbormasterPlanController',
     'HarbormasterPublishFragmentBuildStepImplementation' => 'HarbormasterBuildStepImplementation',
     'HarbormasterQueryBuildablesConduitAPIMethod' => 'HarbormasterConduitAPIMethod',
     'HarbormasterQueryBuildsConduitAPIMethod' => 'HarbormasterConduitAPIMethod',
     'HarbormasterRemarkupRule' => 'PhabricatorObjectRemarkupRule',
     'HarbormasterSchemaSpec' => 'PhabricatorConfigSchemaSpec',
     'HarbormasterScratchTable' => 'HarbormasterDAO',
     'HarbormasterSendMessageConduitAPIMethod' => 'HarbormasterConduitAPIMethod',
     'HarbormasterSleepBuildStepImplementation' => 'HarbormasterBuildStepImplementation',
     'HarbormasterStepAddController' => 'HarbormasterController',
     'HarbormasterStepDeleteController' => 'HarbormasterController',
     'HarbormasterStepEditController' => 'HarbormasterController',
     'HarbormasterTargetWorker' => 'HarbormasterWorker',
     'HarbormasterThrowExceptionBuildStep' => 'HarbormasterBuildStepImplementation',
     'HarbormasterUIEventListener' => 'PhabricatorEventListener',
     'HarbormasterUploadArtifactBuildStepImplementation' => 'HarbormasterBuildStepImplementation',
     'HarbormasterWaitForPreviousBuildStepImplementation' => 'HarbormasterBuildStepImplementation',
     'HarbormasterWorker' => 'PhabricatorWorker',
     'HeraldAction' => 'HeraldDAO',
+    'HeraldAdapter' => 'Phobject',
     'HeraldApplyTranscript' => 'Phobject',
     'HeraldCommitAdapter' => 'HeraldAdapter',
     'HeraldCondition' => 'HeraldDAO',
+    'HeraldConditionTranscript' => 'Phobject',
     'HeraldController' => 'PhabricatorController',
+    'HeraldCustomAction' => 'Phobject',
     'HeraldDAO' => 'PhabricatorLiskDAO',
     'HeraldDifferentialAdapter' => 'HeraldAdapter',
     'HeraldDifferentialDiffAdapter' => 'HeraldDifferentialAdapter',
     'HeraldDifferentialRevisionAdapter' => 'HeraldDifferentialAdapter',
     'HeraldDisableController' => 'HeraldController',
+    'HeraldEffect' => 'Phobject',
+    'HeraldEngine' => 'Phobject',
     'HeraldInvalidActionException' => 'Exception',
     'HeraldInvalidConditionException' => 'Exception',
     'HeraldManageGlobalRulesCapability' => 'PhabricatorPolicyCapability',
     'HeraldManiphestTaskAdapter' => 'HeraldAdapter',
     'HeraldNewController' => 'HeraldController',
+    'HeraldObjectTranscript' => 'Phobject',
     'HeraldPholioMockAdapter' => 'HeraldAdapter',
     'HeraldPreCommitAdapter' => 'HeraldAdapter',
     'HeraldPreCommitContentAdapter' => 'HeraldPreCommitAdapter',
     'HeraldPreCommitRefAdapter' => 'HeraldPreCommitAdapter',
     'HeraldRecursiveConditionsException' => 'Exception',
     'HeraldRemarkupRule' => 'PhabricatorObjectRemarkupRule',
+    'HeraldRepetitionPolicyConfig' => 'Phobject',
     'HeraldRule' => array(
       'HeraldDAO',
       'PhabricatorApplicationTransactionInterface',
       'PhabricatorFlaggableInterface',
       'PhabricatorPolicyInterface',
       'PhabricatorDestructibleInterface',
     ),
     'HeraldRuleController' => 'HeraldController',
     'HeraldRuleEditor' => 'PhabricatorApplicationTransactionEditor',
     'HeraldRuleListController' => 'HeraldController',
     'HeraldRulePHIDType' => 'PhabricatorPHIDType',
     'HeraldRuleQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'HeraldRuleSearchEngine' => 'PhabricatorApplicationSearchEngine',
     'HeraldRuleTestCase' => 'PhabricatorTestCase',
     'HeraldRuleTransaction' => 'PhabricatorApplicationTransaction',
     'HeraldRuleTransactionComment' => 'PhabricatorApplicationTransactionComment',
+    'HeraldRuleTranscript' => 'Phobject',
+    'HeraldRuleTypeConfig' => 'Phobject',
     'HeraldRuleViewController' => 'HeraldController',
     'HeraldSchemaSpec' => 'PhabricatorConfigSchemaSpec',
     'HeraldTestConsoleController' => 'HeraldController',
     'HeraldTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
     'HeraldTranscript' => array(
       'HeraldDAO',
       'PhabricatorPolicyInterface',
       'PhabricatorDestructibleInterface',
     ),
     'HeraldTranscriptController' => 'HeraldController',
     'HeraldTranscriptGarbageCollector' => 'PhabricatorGarbageCollector',
     'HeraldTranscriptListController' => 'HeraldController',
     'HeraldTranscriptQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'HeraldTranscriptSearchEngine' => 'PhabricatorApplicationSearchEngine',
     'HeraldTranscriptTestCase' => 'PhabricatorTestCase',
+    'Javelin' => 'Phobject',
     'JavelinReactorUIExample' => 'PhabricatorUIExample',
     'JavelinUIExample' => 'PhabricatorUIExample',
     'JavelinViewExampleServerView' => 'AphrontView',
     'JavelinViewUIExample' => 'PhabricatorUIExample',
     'LegalpadController' => 'PhabricatorController',
     'LegalpadCreateDocumentsCapability' => 'PhabricatorPolicyCapability',
     'LegalpadDAO' => 'PhabricatorLiskDAO',
     'LegalpadDefaultEditCapability' => 'PhabricatorPolicyCapability',
     'LegalpadDefaultViewCapability' => 'PhabricatorPolicyCapability',
     'LegalpadDocument' => array(
       'LegalpadDAO',
       'PhabricatorPolicyInterface',
       'PhabricatorSubscribableInterface',
       'PhabricatorApplicationTransactionInterface',
       'PhabricatorDestructibleInterface',
     ),
     'LegalpadDocumentBody' => array(
       'LegalpadDAO',
       'PhabricatorMarkupInterface',
     ),
     'LegalpadDocumentCommentController' => 'LegalpadController',
     'LegalpadDocumentDatasource' => 'PhabricatorTypeaheadDatasource',
     'LegalpadDocumentDoneController' => 'LegalpadController',
     'LegalpadDocumentEditController' => 'LegalpadController',
     'LegalpadDocumentEditor' => 'PhabricatorApplicationTransactionEditor',
     'LegalpadDocumentListController' => 'LegalpadController',
     'LegalpadDocumentManageController' => 'LegalpadController',
     'LegalpadDocumentQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'LegalpadDocumentRemarkupRule' => 'PhabricatorObjectRemarkupRule',
     'LegalpadDocumentSearchEngine' => 'PhabricatorApplicationSearchEngine',
     'LegalpadDocumentSignController' => 'LegalpadController',
     'LegalpadDocumentSignature' => array(
       'LegalpadDAO',
       'PhabricatorPolicyInterface',
     ),
     'LegalpadDocumentSignatureAddController' => 'LegalpadController',
     'LegalpadDocumentSignatureListController' => 'LegalpadController',
     'LegalpadDocumentSignatureQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'LegalpadDocumentSignatureSearchEngine' => 'PhabricatorApplicationSearchEngine',
     'LegalpadDocumentSignatureVerificationController' => 'LegalpadController',
     'LegalpadDocumentSignatureViewController' => 'LegalpadController',
     'LegalpadMailReceiver' => 'PhabricatorObjectMailReceiver',
     'LegalpadObjectNeedsSignatureEdgeType' => 'PhabricatorEdgeType',
     'LegalpadReplyHandler' => 'PhabricatorApplicationTransactionReplyHandler',
     'LegalpadSchemaSpec' => 'PhabricatorConfigSchemaSpec',
     'LegalpadSignatureNeededByObjectEdgeType' => 'PhabricatorEdgeType',
     'LegalpadTransaction' => 'PhabricatorApplicationTransaction',
     'LegalpadTransactionComment' => 'PhabricatorApplicationTransactionComment',
     'LegalpadTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
     'LegalpadTransactionView' => 'PhabricatorApplicationTransactionView',
     'LiskChunkTestCase' => 'PhabricatorTestCase',
+    'LiskDAO' => 'Phobject',
+    'LiskDAOSet' => 'Phobject',
     'LiskDAOTestCase' => 'PhabricatorTestCase',
     'LiskEphemeralObjectException' => 'Exception',
     'LiskFixtureTestCase' => 'PhabricatorTestCase',
     'LiskIsolationTestCase' => 'PhabricatorTestCase',
     'LiskIsolationTestDAO' => 'LiskDAO',
     'LiskIsolationTestDAOException' => 'Exception',
     'LiskMigrationIterator' => 'PhutilBufferedIterator',
     'LiskRawMigrationIterator' => 'PhutilBufferedIterator',
     'MacroConduitAPIMethod' => 'ConduitAPIMethod',
     'MacroCreateMemeConduitAPIMethod' => 'MacroConduitAPIMethod',
     'MacroQueryConduitAPIMethod' => 'MacroConduitAPIMethod',
     'ManiphestAssignEmailCommand' => 'ManiphestEmailCommand',
     'ManiphestAssigneeDatasource' => 'PhabricatorTypeaheadCompositeDatasource',
     'ManiphestBatchEditController' => 'ManiphestController',
     'ManiphestBulkEditCapability' => 'PhabricatorPolicyCapability',
     'ManiphestClaimEmailCommand' => 'ManiphestEmailCommand',
     'ManiphestCloseEmailCommand' => 'ManiphestEmailCommand',
     'ManiphestConduitAPIMethod' => 'ConduitAPIMethod',
     'ManiphestConfiguredCustomField' => array(
       'ManiphestCustomField',
       'PhabricatorStandardCustomFieldInterface',
     ),
+    'ManiphestConstants' => 'Phobject',
     'ManiphestController' => 'PhabricatorController',
     'ManiphestCreateMailReceiver' => 'PhabricatorMailReceiver',
     'ManiphestCreateTaskConduitAPIMethod' => 'ManiphestConduitAPIMethod',
     'ManiphestCustomField' => 'PhabricatorCustomField',
     'ManiphestCustomFieldNumericIndex' => 'PhabricatorCustomFieldNumericIndexStorage',
     'ManiphestCustomFieldStatusParser' => 'PhabricatorCustomFieldMonogramParser',
     'ManiphestCustomFieldStatusParserTestCase' => 'PhabricatorTestCase',
     'ManiphestCustomFieldStorage' => 'PhabricatorCustomFieldStorage',
     'ManiphestCustomFieldStringIndex' => 'PhabricatorCustomFieldStringIndexStorage',
     'ManiphestDAO' => 'PhabricatorLiskDAO',
     'ManiphestDefaultEditCapability' => 'PhabricatorPolicyCapability',
     'ManiphestDefaultViewCapability' => 'PhabricatorPolicyCapability',
     'ManiphestEditAssignCapability' => 'PhabricatorPolicyCapability',
     'ManiphestEditPoliciesCapability' => 'PhabricatorPolicyCapability',
     'ManiphestEditPriorityCapability' => 'PhabricatorPolicyCapability',
     'ManiphestEditProjectsCapability' => 'PhabricatorPolicyCapability',
     'ManiphestEditStatusCapability' => 'PhabricatorPolicyCapability',
     'ManiphestEmailCommand' => 'MetaMTAEmailTransactionCommand',
     'ManiphestExcelDefaultFormat' => 'ManiphestExcelFormat',
+    'ManiphestExcelFormat' => 'Phobject',
+    'ManiphestExcelFormatTestCase' => 'PhabricatorTestCase',
     'ManiphestExportController' => 'ManiphestController',
     'ManiphestGetTaskTransactionsConduitAPIMethod' => 'ManiphestConduitAPIMethod',
     'ManiphestHovercardEventListener' => 'PhabricatorEventListener',
     'ManiphestInfoConduitAPIMethod' => 'ManiphestConduitAPIMethod',
     'ManiphestNameIndex' => 'ManiphestDAO',
     'ManiphestNameIndexEventListener' => 'PhabricatorEventListener',
     'ManiphestPriorityEmailCommand' => 'ManiphestEmailCommand',
     'ManiphestQueryConduitAPIMethod' => 'ManiphestConduitAPIMethod',
     'ManiphestQueryStatusesConduitAPIMethod' => 'ManiphestConduitAPIMethod',
     'ManiphestRemarkupRule' => 'PhabricatorObjectRemarkupRule',
     'ManiphestReplyHandler' => 'PhabricatorApplicationTransactionReplyHandler',
     'ManiphestReportController' => 'ManiphestController',
     'ManiphestSchemaSpec' => 'PhabricatorConfigSchemaSpec',
     'ManiphestSearchIndexer' => 'PhabricatorSearchDocumentIndexer',
     'ManiphestStatusConfigOptionType' => 'PhabricatorConfigJSONOptionType',
     'ManiphestStatusEmailCommand' => 'ManiphestEmailCommand',
     'ManiphestSubpriorityController' => 'ManiphestController',
     'ManiphestTask' => array(
       'ManiphestDAO',
       'PhabricatorSubscribableInterface',
       'PhabricatorMarkupInterface',
       'PhabricatorPolicyInterface',
       'PhabricatorTokenReceiverInterface',
       'PhabricatorFlaggableInterface',
       'PhabricatorMentionableInterface',
       'PhrequentTrackableInterface',
       'PhabricatorCustomFieldInterface',
       'PhabricatorDestructibleInterface',
       'PhabricatorApplicationTransactionInterface',
       'PhabricatorProjectInterface',
+      'PhabricatorSpacesInterface',
     ),
+    'ManiphestTaskAuthorPolicyRule' => 'PhabricatorPolicyRule',
     'ManiphestTaskClosedStatusDatasource' => 'PhabricatorTypeaheadDatasource',
     'ManiphestTaskDependedOnByTaskEdgeType' => 'PhabricatorEdgeType',
     'ManiphestTaskDependsOnTaskEdgeType' => 'PhabricatorEdgeType',
     'ManiphestTaskDetailController' => 'ManiphestController',
     'ManiphestTaskEditController' => 'ManiphestController',
     'ManiphestTaskHasCommitEdgeType' => 'PhabricatorEdgeType',
     'ManiphestTaskHasMockEdgeType' => 'PhabricatorEdgeType',
     'ManiphestTaskHasRevisionEdgeType' => 'PhabricatorEdgeType',
     'ManiphestTaskListController' => 'ManiphestController',
     'ManiphestTaskListView' => 'ManiphestView',
     'ManiphestTaskMailReceiver' => 'PhabricatorObjectMailReceiver',
     'ManiphestTaskOpenStatusDatasource' => 'PhabricatorTypeaheadDatasource',
     'ManiphestTaskPHIDType' => 'PhabricatorPHIDType',
     'ManiphestTaskPriority' => 'ManiphestConstants',
     'ManiphestTaskPriorityDatasource' => 'PhabricatorTypeaheadDatasource',
     'ManiphestTaskQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'ManiphestTaskResultListView' => 'ManiphestView',
     'ManiphestTaskSearchEngine' => 'PhabricatorApplicationSearchEngine',
     'ManiphestTaskStatus' => 'ManiphestConstants',
     'ManiphestTaskStatusDatasource' => 'PhabricatorTypeaheadDatasource',
     'ManiphestTaskStatusFunctionDatasource' => 'PhabricatorTypeaheadCompositeDatasource',
     'ManiphestTaskStatusTestCase' => 'PhabricatorTestCase',
     'ManiphestTaskTestCase' => 'PhabricatorTestCase',
     'ManiphestTransaction' => 'PhabricatorApplicationTransaction',
     'ManiphestTransactionComment' => 'PhabricatorApplicationTransactionComment',
     'ManiphestTransactionEditor' => 'PhabricatorApplicationTransactionEditor',
     'ManiphestTransactionPreviewController' => 'ManiphestController',
     'ManiphestTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
     'ManiphestTransactionSaveController' => 'ManiphestController',
     'ManiphestUpdateConduitAPIMethod' => 'ManiphestConduitAPIMethod',
     'ManiphestView' => 'AphrontView',
+    'MetaMTAConstants' => 'Phobject',
     'MetaMTAEmailTransactionCommand' => 'Phobject',
+    'MetaMTAEmailTransactionCommandTestCase' => 'PhabricatorTestCase',
     'MetaMTAMailReceivedGarbageCollector' => 'PhabricatorGarbageCollector',
     'MetaMTAMailSentGarbageCollector' => 'PhabricatorGarbageCollector',
     'MetaMTAReceivedMailStatus' => 'MetaMTAConstants',
     'MultimeterContext' => 'MultimeterDimension',
+    'MultimeterControl' => 'Phobject',
     'MultimeterController' => 'PhabricatorController',
     'MultimeterDAO' => 'PhabricatorLiskDAO',
     'MultimeterDimension' => 'MultimeterDAO',
     'MultimeterEvent' => 'MultimeterDAO',
     'MultimeterEventGarbageCollector' => 'PhabricatorGarbageCollector',
     'MultimeterHost' => 'MultimeterDimension',
     'MultimeterLabel' => 'MultimeterDimension',
     'MultimeterSampleController' => 'MultimeterController',
     'MultimeterViewer' => 'MultimeterDimension',
     'NuanceConduitAPIMethod' => 'ConduitAPIMethod',
     'NuanceController' => 'PhabricatorController',
     'NuanceCreateItemConduitAPIMethod' => 'NuanceConduitAPIMethod',
     'NuanceDAO' => 'PhabricatorLiskDAO',
     'NuanceItem' => array(
       'NuanceDAO',
       'PhabricatorPolicyInterface',
     ),
     'NuanceItemEditController' => 'NuanceController',
     'NuanceItemEditor' => 'PhabricatorApplicationTransactionEditor',
     'NuanceItemPHIDType' => 'PhabricatorPHIDType',
     'NuanceItemQuery' => 'NuanceQuery',
     'NuanceItemTransaction' => 'NuanceTransaction',
     'NuanceItemTransactionComment' => 'PhabricatorApplicationTransactionComment',
     'NuanceItemTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
     'NuanceItemViewController' => 'NuanceController',
     'NuancePhabricatorFormSourceDefinition' => 'NuanceSourceDefinition',
     'NuanceQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'NuanceQueue' => array(
       'NuanceDAO',
       'PhabricatorPolicyInterface',
       'PhabricatorApplicationTransactionInterface',
     ),
     'NuanceQueueEditController' => 'NuanceController',
     'NuanceQueueEditor' => 'PhabricatorApplicationTransactionEditor',
     'NuanceQueueItem' => 'NuanceDAO',
     'NuanceQueueListController' => 'NuanceController',
     'NuanceQueuePHIDType' => 'PhabricatorPHIDType',
     'NuanceQueueQuery' => 'NuanceQuery',
     'NuanceQueueSearchEngine' => 'PhabricatorApplicationSearchEngine',
     'NuanceQueueTransaction' => 'NuanceTransaction',
     'NuanceQueueTransactionComment' => 'PhabricatorApplicationTransactionComment',
     'NuanceQueueTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
     'NuanceQueueViewController' => 'NuanceController',
     'NuanceRequestor' => array(
       'NuanceDAO',
       'PhabricatorPolicyInterface',
       'PhabricatorApplicationTransactionInterface',
     ),
     'NuanceRequestorEditController' => 'NuanceController',
     'NuanceRequestorEditor' => 'PhabricatorApplicationTransactionEditor',
     'NuanceRequestorPHIDType' => 'PhabricatorPHIDType',
     'NuanceRequestorQuery' => 'NuanceQuery',
     'NuanceRequestorSource' => 'NuanceDAO',
     'NuanceRequestorTransaction' => 'NuanceTransaction',
     'NuanceRequestorTransactionComment' => 'PhabricatorApplicationTransactionComment',
     'NuanceRequestorTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
     'NuanceRequestorViewController' => 'NuanceController',
     'NuanceSchemaSpec' => 'PhabricatorConfigSchemaSpec',
     'NuanceSource' => array(
       'NuanceDAO',
       'PhabricatorApplicationTransactionInterface',
       'PhabricatorPolicyInterface',
     ),
     'NuanceSourceActionController' => 'NuanceController',
     'NuanceSourceCreateController' => 'NuanceController',
     'NuanceSourceDefaultEditCapability' => 'PhabricatorPolicyCapability',
     'NuanceSourceDefaultViewCapability' => 'PhabricatorPolicyCapability',
     'NuanceSourceDefinition' => 'Phobject',
+    'NuanceSourceDefinitionTestCase' => 'PhabricatorTestCase',
     'NuanceSourceEditController' => 'NuanceController',
     'NuanceSourceEditor' => 'PhabricatorApplicationTransactionEditor',
     'NuanceSourceListController' => 'NuanceController',
     'NuanceSourceManageCapability' => 'PhabricatorPolicyCapability',
     'NuanceSourcePHIDType' => 'PhabricatorPHIDType',
     'NuanceSourceQuery' => 'NuanceQuery',
     'NuanceSourceSearchEngine' => 'PhabricatorApplicationSearchEngine',
     'NuanceSourceTransaction' => 'NuanceTransaction',
     'NuanceSourceTransactionComment' => 'PhabricatorApplicationTransactionComment',
     'NuanceSourceTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
     'NuanceSourceViewController' => 'NuanceController',
     'NuanceTransaction' => 'PhabricatorApplicationTransaction',
     'OwnersConduitAPIMethod' => 'ConduitAPIMethod',
     'OwnersPackageReplyHandler' => 'PhabricatorMailReplyHandler',
     'OwnersQueryConduitAPIMethod' => 'OwnersConduitAPIMethod',
     'PHIDConduitAPIMethod' => 'ConduitAPIMethod',
     'PHIDInfoConduitAPIMethod' => 'PHIDConduitAPIMethod',
     'PHIDLookupConduitAPIMethod' => 'PHIDConduitAPIMethod',
     'PHIDQueryConduitAPIMethod' => 'PHIDConduitAPIMethod',
+    'PHUI' => 'Phobject',
     'PHUIActionPanelExample' => 'PhabricatorUIExample',
     'PHUIActionPanelView' => 'AphrontTagView',
     'PHUIBoxExample' => 'PhabricatorUIExample',
     'PHUIBoxView' => 'AphrontTagView',
     'PHUIButtonBarExample' => 'PhabricatorUIExample',
     'PHUIButtonBarView' => 'AphrontTagView',
     'PHUIButtonExample' => 'PhabricatorUIExample',
     'PHUIButtonView' => 'AphrontTagView',
     'PHUICalendarDayView' => 'AphrontView',
     'PHUICalendarListView' => 'AphrontTagView',
     'PHUICalendarMonthView' => 'AphrontView',
     'PHUICalendarWidgetView' => 'AphrontTagView',
     'PHUIColorPalletteExample' => 'PhabricatorUIExample',
     'PHUICrumbView' => 'AphrontView',
     'PHUICrumbsView' => 'AphrontView',
     'PHUIDiffInlineCommentDetailView' => 'PHUIDiffInlineCommentView',
     'PHUIDiffInlineCommentEditView' => 'PHUIDiffInlineCommentView',
     'PHUIDiffInlineCommentRowScaffold' => 'AphrontView',
     'PHUIDiffInlineCommentTableScaffold' => 'AphrontView',
     'PHUIDiffInlineCommentUndoView' => 'PHUIDiffInlineCommentView',
     'PHUIDiffInlineCommentView' => 'AphrontView',
     'PHUIDiffOneUpInlineCommentRowScaffold' => 'PHUIDiffInlineCommentRowScaffold',
     'PHUIDiffRevealIconView' => 'AphrontView',
     'PHUIDiffTwoUpInlineCommentRowScaffold' => 'PHUIDiffInlineCommentRowScaffold',
     'PHUIDocumentExample' => 'PhabricatorUIExample',
     'PHUIDocumentView' => 'AphrontTagView',
     'PHUIFeedStoryExample' => 'PhabricatorUIExample',
     'PHUIFeedStoryView' => 'AphrontView',
     'PHUIFormDividerControl' => 'AphrontFormControl',
     'PHUIFormFreeformDateControl' => 'AphrontFormControl',
     'PHUIFormInsetView' => 'AphrontView',
     'PHUIFormLayoutView' => 'AphrontView',
     'PHUIFormMultiSubmitControl' => 'AphrontFormControl',
     'PHUIFormPageView' => 'AphrontView',
     'PHUIHandleListView' => 'AphrontTagView',
     'PHUIHandleTagListView' => 'AphrontTagView',
     'PHUIHandleView' => 'AphrontView',
     'PHUIHeaderView' => 'AphrontTagView',
     'PHUIIconExample' => 'PhabricatorUIExample',
     'PHUIIconView' => 'AphrontTagView',
     'PHUIImageMaskExample' => 'PhabricatorUIExample',
     'PHUIImageMaskView' => 'AphrontTagView',
     'PHUIInfoExample' => 'PhabricatorUIExample',
     'PHUIInfoPanelExample' => 'PhabricatorUIExample',
     'PHUIInfoPanelView' => 'AphrontView',
     'PHUIInfoView' => 'AphrontView',
     'PHUIListExample' => 'PhabricatorUIExample',
     'PHUIListItemView' => 'AphrontTagView',
     'PHUIListView' => 'AphrontTagView',
     'PHUIListViewTestCase' => 'PhabricatorTestCase',
     'PHUIObjectBoxView' => 'AphrontView',
     'PHUIObjectItemListExample' => 'PhabricatorUIExample',
     'PHUIObjectItemListView' => 'AphrontTagView',
     'PHUIObjectItemView' => 'AphrontTagView',
     'PHUIPagedFormView' => 'AphrontView',
     'PHUIPagerView' => 'AphrontView',
     'PHUIPinboardItemView' => 'AphrontView',
     'PHUIPinboardView' => 'AphrontView',
     'PHUIPropertyGroupView' => 'AphrontTagView',
     'PHUIPropertyListExample' => 'PhabricatorUIExample',
     'PHUIPropertyListView' => 'AphrontView',
     'PHUIRemarkupPreviewPanel' => 'AphrontTagView',
     'PHUISpacesNamespaceContextView' => 'AphrontView',
     'PHUIStatusItemView' => 'AphrontTagView',
     'PHUIStatusListView' => 'AphrontTagView',
     'PHUITagExample' => 'PhabricatorUIExample',
     'PHUITagView' => 'AphrontTagView',
     'PHUITextExample' => 'PhabricatorUIExample',
     'PHUITextView' => 'AphrontTagView',
     'PHUITimelineEventView' => 'AphrontView',
     'PHUITimelineExample' => 'PhabricatorUIExample',
     'PHUITimelineView' => 'AphrontView',
     'PHUITypeaheadExample' => 'PhabricatorUIExample',
     'PHUIWorkboardView' => 'AphrontTagView',
     'PHUIWorkpanelView' => 'AphrontTagView',
     'PassphraseAbstractKey' => 'Phobject',
     'PassphraseConduitAPIMethod' => 'ConduitAPIMethod',
     'PassphraseController' => 'PhabricatorController',
     'PassphraseCredential' => array(
       'PassphraseDAO',
       'PhabricatorApplicationTransactionInterface',
       'PhabricatorPolicyInterface',
       'PhabricatorDestructibleInterface',
     ),
     'PassphraseCredentialConduitController' => 'PassphraseController',
     'PassphraseCredentialControl' => 'AphrontFormControl',
     'PassphraseCredentialCreateController' => 'PassphraseController',
     'PassphraseCredentialDestroyController' => 'PassphraseController',
     'PassphraseCredentialEditController' => 'PassphraseController',
     'PassphraseCredentialListController' => 'PassphraseController',
     'PassphraseCredentialLockController' => 'PassphraseController',
     'PassphraseCredentialPHIDType' => 'PhabricatorPHIDType',
     'PassphraseCredentialPublicController' => 'PassphraseController',
     'PassphraseCredentialQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'PassphraseCredentialRevealController' => 'PassphraseController',
     'PassphraseCredentialSearchEngine' => 'PhabricatorApplicationSearchEngine',
     'PassphraseCredentialTransaction' => 'PhabricatorApplicationTransaction',
     'PassphraseCredentialTransactionEditor' => 'PhabricatorApplicationTransactionEditor',
     'PassphraseCredentialTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
     'PassphraseCredentialType' => 'Phobject',
-    'PassphraseCredentialTypePassword' => 'PassphraseCredentialType',
-    'PassphraseCredentialTypeSSHGeneratedKey' => 'PassphraseCredentialTypeSSHPrivateKey',
-    'PassphraseCredentialTypeSSHPrivateKey' => 'PassphraseCredentialType',
-    'PassphraseCredentialTypeSSHPrivateKeyFile' => 'PassphraseCredentialTypeSSHPrivateKey',
-    'PassphraseCredentialTypeSSHPrivateKeyText' => 'PassphraseCredentialTypeSSHPrivateKey',
+    'PassphraseCredentialTypeTestCase' => 'PhabricatorTestCase',
     'PassphraseCredentialViewController' => 'PassphraseController',
     'PassphraseDAO' => 'PhabricatorLiskDAO',
+    'PassphrasePasswordCredentialType' => 'PassphraseCredentialType',
     'PassphrasePasswordKey' => 'PassphraseAbstractKey',
     'PassphraseQueryConduitAPIMethod' => 'PassphraseConduitAPIMethod',
     'PassphraseRemarkupRule' => 'PhabricatorObjectRemarkupRule',
+    'PassphraseSSHGeneratedKeyCredentialType' => 'PassphraseSSHPrivateKeyCredentialType',
     'PassphraseSSHKey' => 'PassphraseAbstractKey',
+    'PassphraseSSHPrivateKeyCredentialType' => 'PassphraseCredentialType',
+    'PassphraseSSHPrivateKeyFileCredentialType' => 'PassphraseSSHPrivateKeyCredentialType',
+    'PassphraseSSHPrivateKeyTextCredentialType' => 'PassphraseSSHPrivateKeyCredentialType',
     'PassphraseSchemaSpec' => 'PhabricatorConfigSchemaSpec',
     'PassphraseSearchIndexer' => 'PhabricatorSearchDocumentIndexer',
     'PassphraseSecret' => 'PassphraseDAO',
     'PasteConduitAPIMethod' => 'ConduitAPIMethod',
     'PasteCreateConduitAPIMethod' => 'PasteConduitAPIMethod',
     'PasteCreateMailReceiver' => 'PhabricatorMailReceiver',
     'PasteDefaultEditCapability' => 'PhabricatorPolicyCapability',
     'PasteDefaultViewCapability' => 'PhabricatorPolicyCapability',
     'PasteEmbedView' => 'AphrontView',
     'PasteInfoConduitAPIMethod' => 'PasteConduitAPIMethod',
     'PasteMailReceiver' => 'PhabricatorObjectMailReceiver',
     'PasteQueryConduitAPIMethod' => 'PasteConduitAPIMethod',
     'PasteReplyHandler' => 'PhabricatorApplicationTransactionReplyHandler',
     'PeopleBrowseUserDirectoryCapability' => 'PhabricatorPolicyCapability',
     'PeopleCreateUsersCapability' => 'PhabricatorPolicyCapability',
     'PeopleUserLogGarbageCollector' => 'PhabricatorGarbageCollector',
     'Phabricator404Controller' => 'PhabricatorController',
     'PhabricatorAWSConfigOptions' => 'PhabricatorApplicationConfigOptions',
     'PhabricatorAccessControlTestCase' => 'PhabricatorTestCase',
+    'PhabricatorAccessLog' => 'Phobject',
     'PhabricatorAccessLogConfigOptions' => 'PhabricatorApplicationConfigOptions',
     'PhabricatorAccountSettingsPanel' => 'PhabricatorSettingsPanel',
     'PhabricatorActionListView' => 'AphrontView',
     'PhabricatorActionView' => 'AphrontView',
     'PhabricatorActivitySettingsPanel' => 'PhabricatorSettingsPanel',
     'PhabricatorAdministratorsPolicyRule' => 'PhabricatorPolicyRule',
     'PhabricatorAlmanacApplication' => 'PhabricatorApplication',
     'PhabricatorAmazonAuthProvider' => 'PhabricatorOAuth2AuthProvider',
     'PhabricatorAnchorView' => 'AphrontView',
     'PhabricatorAphlictManagementDebugWorkflow' => 'PhabricatorAphlictManagementWorkflow',
     'PhabricatorAphlictManagementRestartWorkflow' => 'PhabricatorAphlictManagementWorkflow',
     'PhabricatorAphlictManagementStartWorkflow' => 'PhabricatorAphlictManagementWorkflow',
     'PhabricatorAphlictManagementStatusWorkflow' => 'PhabricatorAphlictManagementWorkflow',
     'PhabricatorAphlictManagementStopWorkflow' => 'PhabricatorAphlictManagementWorkflow',
     'PhabricatorAphlictManagementWorkflow' => 'PhabricatorManagementWorkflow',
     'PhabricatorAphlictSetupCheck' => 'PhabricatorSetupCheck',
     'PhabricatorAphrontBarUIExample' => 'PhabricatorUIExample',
     'PhabricatorAphrontViewTestCase' => 'PhabricatorTestCase',
     'PhabricatorAppSearchEngine' => 'PhabricatorApplicationSearchEngine',
-    'PhabricatorApplication' => 'PhabricatorPolicyInterface',
+    'PhabricatorApplication' => array(
+      'Phobject',
+      'PhabricatorPolicyInterface',
+    ),
     'PhabricatorApplicationApplicationPHIDType' => 'PhabricatorPHIDType',
     'PhabricatorApplicationConfigOptions' => 'Phobject',
     'PhabricatorApplicationConfigurationPanel' => 'Phobject',
+    'PhabricatorApplicationConfigurationPanelTestCase' => 'PhabricatorTestCase',
     'PhabricatorApplicationDatasource' => 'PhabricatorTypeaheadDatasource',
     'PhabricatorApplicationDetailViewController' => 'PhabricatorApplicationsController',
     'PhabricatorApplicationEditController' => 'PhabricatorApplicationsController',
     'PhabricatorApplicationEmailCommandsController' => 'PhabricatorApplicationsController',
     'PhabricatorApplicationLaunchView' => 'AphrontTagView',
     'PhabricatorApplicationPanelController' => 'PhabricatorApplicationsController',
     'PhabricatorApplicationQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'PhabricatorApplicationSearchController' => 'PhabricatorSearchBaseController',
     'PhabricatorApplicationSearchEngine' => 'Phobject',
+    'PhabricatorApplicationSearchEngineTestCase' => 'PhabricatorTestCase',
     'PhabricatorApplicationStatusView' => 'AphrontView',
+    'PhabricatorApplicationTestCase' => 'PhabricatorTestCase',
     'PhabricatorApplicationTransaction' => array(
       'PhabricatorLiskDAO',
       'PhabricatorPolicyInterface',
       'PhabricatorDestructibleInterface',
     ),
     'PhabricatorApplicationTransactionComment' => array(
       'PhabricatorLiskDAO',
       'PhabricatorMarkupInterface',
       'PhabricatorPolicyInterface',
       'PhabricatorDestructibleInterface',
     ),
     'PhabricatorApplicationTransactionCommentEditController' => 'PhabricatorApplicationTransactionController',
     'PhabricatorApplicationTransactionCommentEditor' => 'PhabricatorEditor',
     'PhabricatorApplicationTransactionCommentHistoryController' => 'PhabricatorApplicationTransactionController',
     'PhabricatorApplicationTransactionCommentQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'PhabricatorApplicationTransactionCommentQuoteController' => 'PhabricatorApplicationTransactionController',
     'PhabricatorApplicationTransactionCommentRawController' => 'PhabricatorApplicationTransactionController',
     'PhabricatorApplicationTransactionCommentRemoveController' => 'PhabricatorApplicationTransactionController',
     'PhabricatorApplicationTransactionCommentView' => 'AphrontView',
     'PhabricatorApplicationTransactionController' => 'PhabricatorController',
     'PhabricatorApplicationTransactionDetailController' => 'PhabricatorApplicationTransactionController',
     'PhabricatorApplicationTransactionEditor' => 'PhabricatorEditor',
     'PhabricatorApplicationTransactionFeedStory' => 'PhabricatorFeedStory',
     'PhabricatorApplicationTransactionNoEffectException' => 'Exception',
     'PhabricatorApplicationTransactionNoEffectResponse' => 'AphrontProxyResponse',
     'PhabricatorApplicationTransactionPublishWorker' => 'PhabricatorWorker',
     'PhabricatorApplicationTransactionQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'PhabricatorApplicationTransactionReplyHandler' => 'PhabricatorMailReplyHandler',
     'PhabricatorApplicationTransactionResponse' => 'AphrontProxyResponse',
     'PhabricatorApplicationTransactionShowOlderController' => 'PhabricatorApplicationTransactionController',
     'PhabricatorApplicationTransactionStructureException' => 'Exception',
     'PhabricatorApplicationTransactionTemplatedCommentQuery' => 'PhabricatorApplicationTransactionCommentQuery',
     'PhabricatorApplicationTransactionTextDiffDetailView' => 'AphrontView',
     'PhabricatorApplicationTransactionTransactionPHIDType' => 'PhabricatorPHIDType',
     'PhabricatorApplicationTransactionValidationError' => 'Phobject',
     'PhabricatorApplicationTransactionValidationException' => 'Exception',
     'PhabricatorApplicationTransactionValidationResponse' => 'AphrontProxyResponse',
     'PhabricatorApplicationTransactionValueController' => 'PhabricatorApplicationTransactionController',
     'PhabricatorApplicationTransactionView' => 'AphrontView',
     'PhabricatorApplicationUninstallController' => 'PhabricatorApplicationsController',
     'PhabricatorApplicationsApplication' => 'PhabricatorApplication',
     'PhabricatorApplicationsController' => 'PhabricatorController',
     'PhabricatorApplicationsListController' => 'PhabricatorApplicationsController',
     'PhabricatorAsanaAuthProvider' => 'PhabricatorOAuth2AuthProvider',
     'PhabricatorAsanaConfigOptions' => 'PhabricatorApplicationConfigOptions',
     'PhabricatorAsanaSubtaskHasObjectEdgeType' => 'PhabricatorEdgeType',
     'PhabricatorAsanaTaskHasObjectEdgeType' => 'PhabricatorEdgeType',
+    'PhabricatorAuditActionConstants' => 'Phobject',
     'PhabricatorAuditAddCommentController' => 'PhabricatorAuditController',
     'PhabricatorAuditApplication' => 'PhabricatorApplication',
     'PhabricatorAuditCommentEditor' => 'PhabricatorEditor',
+    'PhabricatorAuditCommitStatusConstants' => 'Phobject',
     'PhabricatorAuditController' => 'PhabricatorController',
     'PhabricatorAuditEditor' => 'PhabricatorApplicationTransactionEditor',
-    'PhabricatorAuditInlineComment' => 'PhabricatorInlineCommentInterface',
+    'PhabricatorAuditInlineComment' => array(
+      'Phobject',
+      'PhabricatorInlineCommentInterface',
+    ),
     'PhabricatorAuditListController' => 'PhabricatorAuditController',
     'PhabricatorAuditListView' => 'AphrontView',
     'PhabricatorAuditMailReceiver' => 'PhabricatorObjectMailReceiver',
     'PhabricatorAuditManagementDeleteWorkflow' => 'PhabricatorAuditManagementWorkflow',
     'PhabricatorAuditManagementWorkflow' => 'PhabricatorManagementWorkflow',
     'PhabricatorAuditPreviewController' => 'PhabricatorAuditController',
     'PhabricatorAuditReplyHandler' => 'PhabricatorApplicationTransactionReplyHandler',
+    'PhabricatorAuditStatusConstants' => 'Phobject',
     'PhabricatorAuditTransaction' => 'PhabricatorApplicationTransaction',
     'PhabricatorAuditTransactionComment' => 'PhabricatorApplicationTransactionComment',
     'PhabricatorAuditTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
     'PhabricatorAuditTransactionView' => 'PhabricatorApplicationTransactionView',
     'PhabricatorAuthAccountView' => 'AphrontView',
     'PhabricatorAuthApplication' => 'PhabricatorApplication',
     'PhabricatorAuthAuthFactorPHIDType' => 'PhabricatorPHIDType',
     'PhabricatorAuthAuthProviderPHIDType' => 'PhabricatorPHIDType',
     'PhabricatorAuthConduitAPIMethod' => 'ConduitAPIMethod',
     'PhabricatorAuthConfirmLinkController' => 'PhabricatorAuthController',
     'PhabricatorAuthController' => 'PhabricatorController',
     'PhabricatorAuthDAO' => 'PhabricatorLiskDAO',
     'PhabricatorAuthDisableController' => 'PhabricatorAuthProviderConfigController',
     'PhabricatorAuthDowngradeSessionController' => 'PhabricatorAuthController',
     'PhabricatorAuthEditController' => 'PhabricatorAuthProviderConfigController',
     'PhabricatorAuthFactor' => 'Phobject',
     'PhabricatorAuthFactorConfig' => 'PhabricatorAuthDAO',
+    'PhabricatorAuthFactorTestCase' => 'PhabricatorTestCase',
     'PhabricatorAuthFinishController' => 'PhabricatorAuthController',
     'PhabricatorAuthHighSecurityRequiredException' => 'Exception',
+    'PhabricatorAuthHighSecurityToken' => 'Phobject',
     'PhabricatorAuthInvite' => array(
       'PhabricatorUserDAO',
       'PhabricatorPolicyInterface',
     ),
     'PhabricatorAuthInviteAccountException' => 'PhabricatorAuthInviteDialogException',
     'PhabricatorAuthInviteAction' => 'Phobject',
     'PhabricatorAuthInviteActionTableView' => 'AphrontView',
     'PhabricatorAuthInviteController' => 'PhabricatorAuthController',
     'PhabricatorAuthInviteDialogException' => 'PhabricatorAuthInviteException',
     'PhabricatorAuthInviteEngine' => 'Phobject',
     'PhabricatorAuthInviteException' => 'Exception',
     'PhabricatorAuthInviteInvalidException' => 'PhabricatorAuthInviteDialogException',
     'PhabricatorAuthInviteLoginException' => 'PhabricatorAuthInviteDialogException',
     'PhabricatorAuthInvitePHIDType' => 'PhabricatorPHIDType',
     'PhabricatorAuthInviteQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'PhabricatorAuthInviteRegisteredException' => 'PhabricatorAuthInviteException',
     'PhabricatorAuthInviteSearchEngine' => 'PhabricatorApplicationSearchEngine',
     'PhabricatorAuthInviteTestCase' => 'PhabricatorTestCase',
     'PhabricatorAuthInviteVerifyException' => 'PhabricatorAuthInviteDialogException',
     'PhabricatorAuthInviteWorker' => 'PhabricatorWorker',
     'PhabricatorAuthLinkController' => 'PhabricatorAuthController',
     'PhabricatorAuthListController' => 'PhabricatorAuthProviderConfigController',
     'PhabricatorAuthLoginController' => 'PhabricatorAuthController',
     'PhabricatorAuthManagementCachePKCS8Workflow' => 'PhabricatorAuthManagementWorkflow',
     'PhabricatorAuthManagementLDAPWorkflow' => 'PhabricatorAuthManagementWorkflow',
     'PhabricatorAuthManagementListFactorsWorkflow' => 'PhabricatorAuthManagementWorkflow',
     'PhabricatorAuthManagementRecoverWorkflow' => 'PhabricatorAuthManagementWorkflow',
     'PhabricatorAuthManagementRefreshWorkflow' => 'PhabricatorAuthManagementWorkflow',
     'PhabricatorAuthManagementStripWorkflow' => 'PhabricatorAuthManagementWorkflow',
     'PhabricatorAuthManagementTrustOAuthClientWorkflow' => 'PhabricatorAuthManagementWorkflow',
     'PhabricatorAuthManagementUntrustOAuthClientWorkflow' => 'PhabricatorAuthManagementWorkflow',
     'PhabricatorAuthManagementVerifyWorkflow' => 'PhabricatorAuthManagementWorkflow',
     'PhabricatorAuthManagementWorkflow' => 'PhabricatorManagementWorkflow',
     'PhabricatorAuthNeedsApprovalController' => 'PhabricatorAuthController',
     'PhabricatorAuthNeedsMultiFactorController' => 'PhabricatorAuthController',
     'PhabricatorAuthNewController' => 'PhabricatorAuthProviderConfigController',
     'PhabricatorAuthOldOAuthRedirectController' => 'PhabricatorAuthController',
     'PhabricatorAuthOneTimeLoginController' => 'PhabricatorAuthController',
+    'PhabricatorAuthProvider' => 'Phobject',
     'PhabricatorAuthProviderConfig' => array(
       'PhabricatorAuthDAO',
       'PhabricatorApplicationTransactionInterface',
       'PhabricatorPolicyInterface',
     ),
     'PhabricatorAuthProviderConfigController' => 'PhabricatorAuthController',
     'PhabricatorAuthProviderConfigEditor' => 'PhabricatorApplicationTransactionEditor',
     'PhabricatorAuthProviderConfigQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'PhabricatorAuthProviderConfigTransaction' => 'PhabricatorApplicationTransaction',
     'PhabricatorAuthProviderConfigTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
     'PhabricatorAuthQueryPublicKeysConduitAPIMethod' => 'PhabricatorAuthConduitAPIMethod',
     'PhabricatorAuthRegisterController' => 'PhabricatorAuthController',
     'PhabricatorAuthRevokeTokenController' => 'PhabricatorAuthController',
     'PhabricatorAuthSSHKey' => array(
       'PhabricatorAuthDAO',
       'PhabricatorPolicyInterface',
     ),
     'PhabricatorAuthSSHKeyController' => 'PhabricatorAuthController',
     'PhabricatorAuthSSHKeyDeleteController' => 'PhabricatorAuthSSHKeyController',
     'PhabricatorAuthSSHKeyEditController' => 'PhabricatorAuthSSHKeyController',
     'PhabricatorAuthSSHKeyGenerateController' => 'PhabricatorAuthSSHKeyController',
     'PhabricatorAuthSSHKeyQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'PhabricatorAuthSSHKeyTableView' => 'AphrontView',
     'PhabricatorAuthSSHPublicKey' => 'Phobject',
     'PhabricatorAuthSession' => array(
       'PhabricatorAuthDAO',
       'PhabricatorPolicyInterface',
     ),
     'PhabricatorAuthSessionEngine' => 'Phobject',
     'PhabricatorAuthSessionGarbageCollector' => 'PhabricatorGarbageCollector',
     'PhabricatorAuthSessionQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'PhabricatorAuthSetupCheck' => 'PhabricatorSetupCheck',
     'PhabricatorAuthStartController' => 'PhabricatorAuthController',
     'PhabricatorAuthTemporaryToken' => array(
       'PhabricatorAuthDAO',
       'PhabricatorPolicyInterface',
     ),
     'PhabricatorAuthTemporaryTokenGarbageCollector' => 'PhabricatorGarbageCollector',
     'PhabricatorAuthTemporaryTokenQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'PhabricatorAuthTerminateSessionController' => 'PhabricatorAuthController',
     'PhabricatorAuthTryFactorAction' => 'PhabricatorSystemAction',
     'PhabricatorAuthUnlinkController' => 'PhabricatorAuthController',
     'PhabricatorAuthValidateController' => 'PhabricatorAuthController',
     'PhabricatorAuthenticationConfigOptions' => 'PhabricatorApplicationConfigOptions',
     'PhabricatorAutoEventListener' => 'PhabricatorEventListener',
     'PhabricatorBarePageUIExample' => 'PhabricatorUIExample',
     'PhabricatorBarePageView' => 'AphrontPageView',
     'PhabricatorBaseURISetupCheck' => 'PhabricatorSetupCheck',
     'PhabricatorBcryptPasswordHasher' => 'PhabricatorPasswordHasher',
     'PhabricatorBinariesSetupCheck' => 'PhabricatorSetupCheck',
     'PhabricatorBitbucketAuthProvider' => 'PhabricatorOAuth1AuthProvider',
     'PhabricatorBot' => 'PhabricatorDaemon',
     'PhabricatorBotChannel' => 'PhabricatorBotTarget',
     'PhabricatorBotDebugLogHandler' => 'PhabricatorBotHandler',
     'PhabricatorBotFeedNotificationHandler' => 'PhabricatorBotHandler',
     'PhabricatorBotFlowdockProtocolAdapter' => 'PhabricatorStreamingProtocolAdapter',
+    'PhabricatorBotHandler' => 'Phobject',
     'PhabricatorBotLogHandler' => 'PhabricatorBotHandler',
     'PhabricatorBotMacroHandler' => 'PhabricatorBotHandler',
+    'PhabricatorBotMessage' => 'Phobject',
     'PhabricatorBotObjectNameHandler' => 'PhabricatorBotHandler',
     'PhabricatorBotSymbolHandler' => 'PhabricatorBotHandler',
+    'PhabricatorBotTarget' => 'Phobject',
     'PhabricatorBotUser' => 'PhabricatorBotTarget',
     'PhabricatorBotWhatsNewHandler' => 'PhabricatorBotHandler',
     'PhabricatorBritishEnglishTranslation' => 'PhutilTranslation',
     'PhabricatorBuiltinPatchList' => 'PhabricatorSQLPatchList',
     'PhabricatorBusyUIExample' => 'PhabricatorUIExample',
     'PhabricatorCacheDAO' => 'PhabricatorLiskDAO',
     'PhabricatorCacheGeneralGarbageCollector' => 'PhabricatorGarbageCollector',
     'PhabricatorCacheManagementPurgeWorkflow' => 'PhabricatorCacheManagementWorkflow',
     'PhabricatorCacheManagementWorkflow' => 'PhabricatorManagementWorkflow',
     'PhabricatorCacheMarkupGarbageCollector' => 'PhabricatorGarbageCollector',
     'PhabricatorCacheSchemaSpec' => 'PhabricatorConfigSchemaSpec',
     'PhabricatorCacheSetupCheck' => 'PhabricatorSetupCheck',
     'PhabricatorCacheSpec' => 'Phobject',
     'PhabricatorCacheTTLGarbageCollector' => 'PhabricatorGarbageCollector',
+    'PhabricatorCaches' => 'Phobject',
     'PhabricatorCachesTestCase' => 'PhabricatorTestCase',
     'PhabricatorCalendarApplication' => 'PhabricatorApplication',
     'PhabricatorCalendarController' => 'PhabricatorController',
     'PhabricatorCalendarDAO' => 'PhabricatorLiskDAO',
     'PhabricatorCalendarEvent' => array(
       'PhabricatorCalendarDAO',
       'PhabricatorPolicyInterface',
       'PhabricatorMarkupInterface',
       'PhabricatorApplicationTransactionInterface',
       'PhabricatorSubscribableInterface',
       'PhabricatorTokenReceiverInterface',
       'PhabricatorDestructibleInterface',
       'PhabricatorMentionableInterface',
       'PhabricatorFlaggableInterface',
     ),
     'PhabricatorCalendarEventCancelController' => 'PhabricatorCalendarController',
     'PhabricatorCalendarEventCommentController' => 'PhabricatorCalendarController',
     'PhabricatorCalendarEventDragController' => 'PhabricatorCalendarController',
     'PhabricatorCalendarEventEditController' => 'PhabricatorCalendarController',
     'PhabricatorCalendarEventEditIconController' => 'PhabricatorCalendarController',
     'PhabricatorCalendarEventEditor' => 'PhabricatorApplicationTransactionEditor',
     'PhabricatorCalendarEventEmailCommand' => 'MetaMTAEmailTransactionCommand',
     'PhabricatorCalendarEventInvitee' => array(
       'PhabricatorCalendarDAO',
       'PhabricatorPolicyInterface',
     ),
     'PhabricatorCalendarEventInviteeQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'PhabricatorCalendarEventJoinController' => 'PhabricatorCalendarController',
     'PhabricatorCalendarEventListController' => 'PhabricatorCalendarController',
     'PhabricatorCalendarEventMailReceiver' => 'PhabricatorObjectMailReceiver',
     'PhabricatorCalendarEventPHIDType' => 'PhabricatorPHIDType',
     'PhabricatorCalendarEventQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'PhabricatorCalendarEventRSVPEmailCommand' => 'PhabricatorCalendarEventEmailCommand',
     'PhabricatorCalendarEventSearchEngine' => 'PhabricatorApplicationSearchEngine',
     'PhabricatorCalendarEventSearchIndexer' => 'PhabricatorSearchDocumentIndexer',
     'PhabricatorCalendarEventTransaction' => 'PhabricatorApplicationTransaction',
     'PhabricatorCalendarEventTransactionComment' => 'PhabricatorApplicationTransactionComment',
     'PhabricatorCalendarEventTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
     'PhabricatorCalendarEventViewController' => 'PhabricatorCalendarController',
     'PhabricatorCalendarHoliday' => 'PhabricatorCalendarDAO',
     'PhabricatorCalendarHolidayTestCase' => 'PhabricatorTestCase',
     'PhabricatorCalendarIcon' => 'Phobject',
     'PhabricatorCalendarRemarkupRule' => 'PhabricatorObjectRemarkupRule',
     'PhabricatorCalendarReplyHandler' => 'PhabricatorApplicationTransactionReplyHandler',
     'PhabricatorCalendarSchemaSpec' => 'PhabricatorConfigSchemaSpec',
     'PhabricatorCampfireProtocolAdapter' => 'PhabricatorStreamingProtocolAdapter',
     'PhabricatorCelerityApplication' => 'PhabricatorApplication',
     'PhabricatorCelerityTestCase' => 'PhabricatorTestCase',
     'PhabricatorChangeParserTestCase' => 'PhabricatorWorkingCopyTestCase',
     'PhabricatorChangesetResponse' => 'AphrontProxyResponse',
     'PhabricatorChatLogApplication' => 'PhabricatorApplication',
     'PhabricatorChatLogChannel' => array(
       'PhabricatorChatLogDAO',
       'PhabricatorPolicyInterface',
     ),
     'PhabricatorChatLogChannelListController' => 'PhabricatorChatLogController',
     'PhabricatorChatLogChannelLogController' => 'PhabricatorChatLogController',
     'PhabricatorChatLogChannelQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'PhabricatorChatLogController' => 'PhabricatorController',
     'PhabricatorChatLogDAO' => 'PhabricatorLiskDAO',
     'PhabricatorChatLogEvent' => array(
       'PhabricatorChatLogDAO',
       'PhabricatorPolicyInterface',
     ),
     'PhabricatorChatLogQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'PhabricatorChunkedFileStorageEngine' => 'PhabricatorFileStorageEngine',
     'PhabricatorClusterConfigOptions' => 'PhabricatorApplicationConfigOptions',
     'PhabricatorCommitBranchesField' => 'PhabricatorCommitCustomField',
     'PhabricatorCommitCustomField' => 'PhabricatorCustomField',
     'PhabricatorCommitMergedCommitsField' => 'PhabricatorCommitCustomField',
     'PhabricatorCommitRepositoryField' => 'PhabricatorCommitCustomField',
     'PhabricatorCommitSearchEngine' => 'PhabricatorApplicationSearchEngine',
     'PhabricatorCommitTagsField' => 'PhabricatorCommitCustomField',
     'PhabricatorCommonPasswords' => 'Phobject',
     'PhabricatorConduitAPIController' => 'PhabricatorConduitController',
     'PhabricatorConduitApplication' => 'PhabricatorApplication',
     'PhabricatorConduitCertificateSettingsPanel' => 'PhabricatorSettingsPanel',
     'PhabricatorConduitCertificateToken' => 'PhabricatorConduitDAO',
     'PhabricatorConduitConnectionLog' => 'PhabricatorConduitDAO',
     'PhabricatorConduitConsoleController' => 'PhabricatorConduitController',
     'PhabricatorConduitController' => 'PhabricatorController',
     'PhabricatorConduitDAO' => 'PhabricatorLiskDAO',
     'PhabricatorConduitListController' => 'PhabricatorConduitController',
     'PhabricatorConduitLogController' => 'PhabricatorConduitController',
     'PhabricatorConduitLogQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'PhabricatorConduitMethodCallLog' => array(
       'PhabricatorConduitDAO',
       'PhabricatorPolicyInterface',
     ),
     'PhabricatorConduitMethodQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'PhabricatorConduitSearchEngine' => 'PhabricatorApplicationSearchEngine',
     'PhabricatorConduitTestCase' => 'PhabricatorTestCase',
     'PhabricatorConduitToken' => array(
       'PhabricatorConduitDAO',
       'PhabricatorPolicyInterface',
     ),
     'PhabricatorConduitTokenController' => 'PhabricatorConduitController',
     'PhabricatorConduitTokenEditController' => 'PhabricatorConduitController',
     'PhabricatorConduitTokenHandshakeController' => 'PhabricatorConduitController',
     'PhabricatorConduitTokenQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'PhabricatorConduitTokenTerminateController' => 'PhabricatorConduitController',
     'PhabricatorConduitTokensSettingsPanel' => 'PhabricatorSettingsPanel',
     'PhabricatorConfigAllController' => 'PhabricatorConfigController',
     'PhabricatorConfigApplication' => 'PhabricatorApplication',
     'PhabricatorConfigCacheController' => 'PhabricatorConfigController',
     'PhabricatorConfigColumnSchema' => 'PhabricatorConfigStorageSchema',
     'PhabricatorConfigConfigPHIDType' => 'PhabricatorPHIDType',
     'PhabricatorConfigController' => 'PhabricatorController',
     'PhabricatorConfigCoreSchemaSpec' => 'PhabricatorConfigSchemaSpec',
     'PhabricatorConfigDatabaseController' => 'PhabricatorConfigController',
     'PhabricatorConfigDatabaseIssueController' => 'PhabricatorConfigDatabaseController',
     'PhabricatorConfigDatabaseSchema' => 'PhabricatorConfigStorageSchema',
     'PhabricatorConfigDatabaseSource' => 'PhabricatorConfigProxySource',
     'PhabricatorConfigDatabaseStatusController' => 'PhabricatorConfigDatabaseController',
     'PhabricatorConfigDefaultSource' => 'PhabricatorConfigProxySource',
     'PhabricatorConfigDictionarySource' => 'PhabricatorConfigSource',
     'PhabricatorConfigEditController' => 'PhabricatorConfigController',
     'PhabricatorConfigEditor' => 'PhabricatorApplicationTransactionEditor',
     'PhabricatorConfigEntry' => array(
       'PhabricatorConfigEntryDAO',
       'PhabricatorApplicationTransactionInterface',
       'PhabricatorPolicyInterface',
     ),
     'PhabricatorConfigEntryDAO' => 'PhabricatorLiskDAO',
     'PhabricatorConfigEntryQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'PhabricatorConfigFileSource' => 'PhabricatorConfigProxySource',
     'PhabricatorConfigGroupController' => 'PhabricatorConfigController',
     'PhabricatorConfigHistoryController' => 'PhabricatorConfigController',
     'PhabricatorConfigIgnoreController' => 'PhabricatorConfigController',
     'PhabricatorConfigIssueListController' => 'PhabricatorConfigController',
     'PhabricatorConfigIssueViewController' => 'PhabricatorConfigController',
+    'PhabricatorConfigJSON' => 'Phobject',
     'PhabricatorConfigJSONOptionType' => 'PhabricatorConfigOptionType',
     'PhabricatorConfigKeySchema' => 'PhabricatorConfigStorageSchema',
     'PhabricatorConfigListController' => 'PhabricatorConfigController',
     'PhabricatorConfigLocalSource' => 'PhabricatorConfigProxySource',
     'PhabricatorConfigManagementDeleteWorkflow' => 'PhabricatorConfigManagementWorkflow',
     'PhabricatorConfigManagementGetWorkflow' => 'PhabricatorConfigManagementWorkflow',
     'PhabricatorConfigManagementListWorkflow' => 'PhabricatorConfigManagementWorkflow',
     'PhabricatorConfigManagementMigrateWorkflow' => 'PhabricatorConfigManagementWorkflow',
     'PhabricatorConfigManagementSetWorkflow' => 'PhabricatorConfigManagementWorkflow',
     'PhabricatorConfigManagementWorkflow' => 'PhabricatorManagementWorkflow',
     'PhabricatorConfigOption' => array(
       'Phobject',
       'PhabricatorMarkupInterface',
     ),
+    'PhabricatorConfigOptionType' => 'Phobject',
     'PhabricatorConfigProxySource' => 'PhabricatorConfigSource',
     'PhabricatorConfigResponse' => 'AphrontStandaloneHTMLResponse',
     'PhabricatorConfigSchemaQuery' => 'Phobject',
     'PhabricatorConfigSchemaSpec' => 'Phobject',
     'PhabricatorConfigServerSchema' => 'PhabricatorConfigStorageSchema',
     'PhabricatorConfigSiteSource' => 'PhabricatorConfigProxySource',
+    'PhabricatorConfigSource' => 'Phobject',
     'PhabricatorConfigStackSource' => 'PhabricatorConfigSource',
     'PhabricatorConfigStorageSchema' => 'Phobject',
     'PhabricatorConfigTableSchema' => 'PhabricatorConfigStorageSchema',
     'PhabricatorConfigTransaction' => 'PhabricatorApplicationTransaction',
     'PhabricatorConfigTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
     'PhabricatorConfigValidationException' => 'Exception',
     'PhabricatorConfigWelcomeController' => 'PhabricatorConfigController',
     'PhabricatorConpherenceApplication' => 'PhabricatorApplication',
     'PhabricatorConpherencePreferencesSettingsPanel' => 'PhabricatorSettingsPanel',
     'PhabricatorConpherenceThreadPHIDType' => 'PhabricatorPHIDType',
     'PhabricatorConsoleApplication' => 'PhabricatorApplication',
+    'PhabricatorContentSource' => 'Phobject',
     'PhabricatorContentSourceView' => 'AphrontView',
     'PhabricatorContributedToObjectEdgeType' => 'PhabricatorEdgeType',
     'PhabricatorController' => 'AphrontController',
     'PhabricatorCookies' => 'Phobject',
     'PhabricatorCoreConfigOptions' => 'PhabricatorApplicationConfigOptions',
     'PhabricatorCountdown' => array(
       'PhabricatorCountdownDAO',
       'PhabricatorPolicyInterface',
     ),
     'PhabricatorCountdownApplication' => 'PhabricatorApplication',
     'PhabricatorCountdownController' => 'PhabricatorController',
     'PhabricatorCountdownCountdownPHIDType' => 'PhabricatorPHIDType',
     'PhabricatorCountdownDAO' => 'PhabricatorLiskDAO',
     'PhabricatorCountdownDefaultViewCapability' => 'PhabricatorPolicyCapability',
     'PhabricatorCountdownDeleteController' => 'PhabricatorCountdownController',
     'PhabricatorCountdownEditController' => 'PhabricatorCountdownController',
     'PhabricatorCountdownListController' => 'PhabricatorCountdownController',
     'PhabricatorCountdownQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'PhabricatorCountdownRemarkupRule' => 'PhabricatorObjectRemarkupRule',
     'PhabricatorCountdownSearchEngine' => 'PhabricatorApplicationSearchEngine',
     'PhabricatorCountdownView' => 'AphrontTagView',
     'PhabricatorCountdownViewController' => 'PhabricatorCountdownController',
     'PhabricatorCredentialsUsedByObjectEdgeType' => 'PhabricatorEdgeType',
     'PhabricatorCursorPagedPolicyAwareQuery' => 'PhabricatorPolicyAwareQuery',
+    'PhabricatorCustomField' => 'Phobject',
+    'PhabricatorCustomFieldAttachment' => 'Phobject',
     'PhabricatorCustomFieldConfigOptionType' => 'PhabricatorConfigOptionType',
     'PhabricatorCustomFieldDataNotAvailableException' => 'Exception',
     'PhabricatorCustomFieldImplementationIncompleteException' => 'Exception',
     'PhabricatorCustomFieldIndexStorage' => 'PhabricatorLiskDAO',
     'PhabricatorCustomFieldList' => 'Phobject',
     'PhabricatorCustomFieldMonogramParser' => 'Phobject',
     'PhabricatorCustomFieldNotAttachedException' => 'Exception',
     'PhabricatorCustomFieldNotProxyException' => 'Exception',
     'PhabricatorCustomFieldNumericIndexStorage' => 'PhabricatorCustomFieldIndexStorage',
     'PhabricatorCustomFieldStorage' => 'PhabricatorLiskDAO',
     'PhabricatorCustomFieldStringIndexStorage' => 'PhabricatorCustomFieldIndexStorage',
     'PhabricatorCustomHeaderConfigType' => 'PhabricatorConfigOptionType',
     'PhabricatorDaemon' => 'PhutilDaemon',
     'PhabricatorDaemonConsoleController' => 'PhabricatorDaemonController',
     'PhabricatorDaemonController' => 'PhabricatorController',
     'PhabricatorDaemonDAO' => 'PhabricatorLiskDAO',
     'PhabricatorDaemonEventListener' => 'PhabricatorEventListener',
     'PhabricatorDaemonLog' => array(
       'PhabricatorDaemonDAO',
       'PhabricatorPolicyInterface',
     ),
     'PhabricatorDaemonLogEvent' => 'PhabricatorDaemonDAO',
     'PhabricatorDaemonLogEventGarbageCollector' => 'PhabricatorGarbageCollector',
     'PhabricatorDaemonLogEventViewController' => 'PhabricatorDaemonController',
     'PhabricatorDaemonLogEventsView' => 'AphrontView',
     'PhabricatorDaemonLogGarbageCollector' => 'PhabricatorGarbageCollector',
     'PhabricatorDaemonLogListController' => 'PhabricatorDaemonController',
     'PhabricatorDaemonLogListView' => 'AphrontView',
     'PhabricatorDaemonLogQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'PhabricatorDaemonLogViewController' => 'PhabricatorDaemonController',
     'PhabricatorDaemonManagementDebugWorkflow' => 'PhabricatorDaemonManagementWorkflow',
     'PhabricatorDaemonManagementLaunchWorkflow' => 'PhabricatorDaemonManagementWorkflow',
     'PhabricatorDaemonManagementListWorkflow' => 'PhabricatorDaemonManagementWorkflow',
     'PhabricatorDaemonManagementLogWorkflow' => 'PhabricatorDaemonManagementWorkflow',
     'PhabricatorDaemonManagementReloadWorkflow' => 'PhabricatorDaemonManagementWorkflow',
     'PhabricatorDaemonManagementRestartWorkflow' => 'PhabricatorDaemonManagementWorkflow',
     'PhabricatorDaemonManagementStartWorkflow' => 'PhabricatorDaemonManagementWorkflow',
     'PhabricatorDaemonManagementStatusWorkflow' => 'PhabricatorDaemonManagementWorkflow',
     'PhabricatorDaemonManagementStopWorkflow' => 'PhabricatorDaemonManagementWorkflow',
     'PhabricatorDaemonManagementWorkflow' => 'PhabricatorManagementWorkflow',
+    'PhabricatorDaemonReference' => 'Phobject',
     'PhabricatorDaemonTaskGarbageCollector' => 'PhabricatorGarbageCollector',
     'PhabricatorDaemonTasksTableView' => 'AphrontView',
     'PhabricatorDaemonsApplication' => 'PhabricatorApplication',
     'PhabricatorDaemonsSetupCheck' => 'PhabricatorSetupCheck',
     'PhabricatorDailyRoutineTriggerClock' => 'PhabricatorTriggerClock',
     'PhabricatorDashboard' => array(
       'PhabricatorDashboardDAO',
       'PhabricatorApplicationTransactionInterface',
       'PhabricatorPolicyInterface',
       'PhabricatorDestructibleInterface',
     ),
     'PhabricatorDashboardAddPanelController' => 'PhabricatorDashboardController',
     'PhabricatorDashboardApplication' => 'PhabricatorApplication',
     'PhabricatorDashboardController' => 'PhabricatorController',
     'PhabricatorDashboardCopyController' => 'PhabricatorDashboardController',
     'PhabricatorDashboardDAO' => 'PhabricatorLiskDAO',
     'PhabricatorDashboardDashboardHasPanelEdgeType' => 'PhabricatorEdgeType',
     'PhabricatorDashboardDashboardPHIDType' => 'PhabricatorPHIDType',
     'PhabricatorDashboardEditController' => 'PhabricatorDashboardController',
     'PhabricatorDashboardHistoryController' => 'PhabricatorDashboardController',
     'PhabricatorDashboardInstall' => 'PhabricatorDashboardDAO',
     'PhabricatorDashboardInstallController' => 'PhabricatorDashboardController',
+    'PhabricatorDashboardLayoutConfig' => 'Phobject',
     'PhabricatorDashboardListController' => 'PhabricatorDashboardController',
     'PhabricatorDashboardManageController' => 'PhabricatorDashboardController',
     'PhabricatorDashboardMovePanelController' => 'PhabricatorDashboardController',
     'PhabricatorDashboardPanel' => array(
       'PhabricatorDashboardDAO',
       'PhabricatorApplicationTransactionInterface',
       'PhabricatorPolicyInterface',
       'PhabricatorCustomFieldInterface',
       'PhabricatorDestructibleInterface',
     ),
     'PhabricatorDashboardPanelArchiveController' => 'PhabricatorDashboardController',
     'PhabricatorDashboardPanelCoreCustomField' => array(
       'PhabricatorDashboardPanelCustomField',
       'PhabricatorStandardCustomFieldInterface',
     ),
     'PhabricatorDashboardPanelCustomField' => 'PhabricatorCustomField',
     'PhabricatorDashboardPanelEditController' => 'PhabricatorDashboardController',
     'PhabricatorDashboardPanelHasDashboardEdgeType' => 'PhabricatorEdgeType',
     'PhabricatorDashboardPanelListController' => 'PhabricatorDashboardController',
     'PhabricatorDashboardPanelPHIDType' => 'PhabricatorPHIDType',
     'PhabricatorDashboardPanelQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'PhabricatorDashboardPanelRenderController' => 'PhabricatorDashboardController',
     'PhabricatorDashboardPanelRenderingEngine' => 'Phobject',
     'PhabricatorDashboardPanelSearchApplicationCustomField' => 'PhabricatorStandardCustomField',
     'PhabricatorDashboardPanelSearchEngine' => 'PhabricatorApplicationSearchEngine',
     'PhabricatorDashboardPanelSearchQueryCustomField' => 'PhabricatorStandardCustomField',
     'PhabricatorDashboardPanelTabsCustomField' => 'PhabricatorStandardCustomField',
     'PhabricatorDashboardPanelTransaction' => 'PhabricatorApplicationTransaction',
     'PhabricatorDashboardPanelTransactionEditor' => 'PhabricatorApplicationTransactionEditor',
     'PhabricatorDashboardPanelTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
     'PhabricatorDashboardPanelType' => 'Phobject',
     'PhabricatorDashboardPanelViewController' => 'PhabricatorDashboardController',
     'PhabricatorDashboardQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'PhabricatorDashboardQueryPanelType' => 'PhabricatorDashboardPanelType',
     'PhabricatorDashboardRemarkupRule' => 'PhabricatorObjectRemarkupRule',
     'PhabricatorDashboardRemovePanelController' => 'PhabricatorDashboardController',
     'PhabricatorDashboardRenderingEngine' => 'Phobject',
     'PhabricatorDashboardSchemaSpec' => 'PhabricatorConfigSchemaSpec',
     'PhabricatorDashboardSearchEngine' => 'PhabricatorApplicationSearchEngine',
     'PhabricatorDashboardTabsPanelType' => 'PhabricatorDashboardPanelType',
     'PhabricatorDashboardTextPanelType' => 'PhabricatorDashboardPanelType',
     'PhabricatorDashboardTransaction' => 'PhabricatorApplicationTransaction',
     'PhabricatorDashboardTransactionEditor' => 'PhabricatorApplicationTransactionEditor',
     'PhabricatorDashboardTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
     'PhabricatorDashboardUninstallController' => 'PhabricatorDashboardController',
     'PhabricatorDashboardViewController' => 'PhabricatorDashboardController',
     'PhabricatorDataCacheSpec' => 'PhabricatorCacheSpec',
     'PhabricatorDataNotAttachedException' => 'Exception',
     'PhabricatorDatabaseSetupCheck' => 'PhabricatorSetupCheck',
     'PhabricatorDateTimeSettingsPanel' => 'PhabricatorSettingsPanel',
     'PhabricatorDebugController' => 'PhabricatorController',
     'PhabricatorDestructionEngine' => 'Phobject',
     'PhabricatorDeveloperConfigOptions' => 'PhabricatorApplicationConfigOptions',
     'PhabricatorDeveloperPreferencesSettingsPanel' => 'PhabricatorSettingsPanel',
     'PhabricatorDiffInlineCommentQuery' => 'PhabricatorApplicationTransactionCommentQuery',
     'PhabricatorDiffPreferencesSettingsPanel' => 'PhabricatorSettingsPanel',
+    'PhabricatorDifferenceEngine' => 'Phobject',
     'PhabricatorDifferentialApplication' => 'PhabricatorApplication',
     'PhabricatorDifferentialConfigOptions' => 'PhabricatorApplicationConfigOptions',
     'PhabricatorDifferentialRevisionTestDataGenerator' => 'PhabricatorTestDataGenerator',
     'PhabricatorDiffusionApplication' => 'PhabricatorApplication',
     'PhabricatorDiffusionConfigOptions' => 'PhabricatorApplicationConfigOptions',
     'PhabricatorDisabledUserController' => 'PhabricatorAuthController',
     'PhabricatorDisplayPreferencesSettingsPanel' => 'PhabricatorSettingsPanel',
     'PhabricatorDisqusAuthProvider' => 'PhabricatorOAuth2AuthProvider',
     'PhabricatorDisqusConfigOptions' => 'PhabricatorApplicationConfigOptions',
     'PhabricatorDivinerApplication' => 'PhabricatorApplication',
     'PhabricatorDoorkeeperApplication' => 'PhabricatorApplication',
     'PhabricatorDraft' => 'PhabricatorDraftDAO',
     'PhabricatorDraftDAO' => 'PhabricatorLiskDAO',
     'PhabricatorDrydockApplication' => 'PhabricatorApplication',
     'PhabricatorEdgeConfig' => 'PhabricatorEdgeConstants',
+    'PhabricatorEdgeConstants' => 'Phobject',
     'PhabricatorEdgeCycleException' => 'Exception',
     'PhabricatorEdgeEditor' => 'Phobject',
     'PhabricatorEdgeGraph' => 'AbstractDirectedGraph',
     'PhabricatorEdgeQuery' => 'PhabricatorQuery',
     'PhabricatorEdgeTestCase' => 'PhabricatorTestCase',
     'PhabricatorEdgeType' => 'Phobject',
+    'PhabricatorEdgeTypeTestCase' => 'PhabricatorTestCase',
     'PhabricatorEditor' => 'Phobject',
     'PhabricatorElasticSearchEngine' => 'PhabricatorSearchEngine',
     'PhabricatorElasticSearchSetupCheck' => 'PhabricatorSetupCheck',
     'PhabricatorEmailAddressesSettingsPanel' => 'PhabricatorSettingsPanel',
     'PhabricatorEmailFormatSettingsPanel' => 'PhabricatorSettingsPanel',
     'PhabricatorEmailLoginController' => 'PhabricatorAuthController',
     'PhabricatorEmailPreferencesSettingsPanel' => 'PhabricatorSettingsPanel',
     'PhabricatorEmailVerificationController' => 'PhabricatorAuthController',
     'PhabricatorEmbedFileRemarkupRule' => 'PhabricatorObjectRemarkupRule',
     'PhabricatorEmojiRemarkupRule' => 'PhutilRemarkupRule',
     'PhabricatorEmptyQueryException' => 'Exception',
+    'PhabricatorEnv' => 'Phobject',
     'PhabricatorEnvTestCase' => 'PhabricatorTestCase',
     'PhabricatorEvent' => 'PhutilEvent',
+    'PhabricatorEventEngine' => 'Phobject',
     'PhabricatorEventListener' => 'PhutilEventListener',
     'PhabricatorEventType' => 'PhutilEventType',
     'PhabricatorExampleEventListener' => 'PhabricatorEventListener',
     'PhabricatorExtendingPhabricatorConfigOptions' => 'PhabricatorApplicationConfigOptions',
     'PhabricatorExtensionsSetupCheck' => 'PhabricatorSetupCheck',
     'PhabricatorExternalAccount' => array(
       'PhabricatorUserDAO',
       'PhabricatorPolicyInterface',
     ),
     'PhabricatorExternalAccountQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'PhabricatorExternalAccountsSettingsPanel' => 'PhabricatorSettingsPanel',
     'PhabricatorExtraConfigSetupCheck' => 'PhabricatorSetupCheck',
     'PhabricatorFacebookAuthProvider' => 'PhabricatorOAuth2AuthProvider',
     'PhabricatorFactAggregate' => 'PhabricatorFactDAO',
     'PhabricatorFactApplication' => 'PhabricatorApplication',
     'PhabricatorFactChartController' => 'PhabricatorFactController',
     'PhabricatorFactController' => 'PhabricatorController',
     'PhabricatorFactCountEngine' => 'PhabricatorFactEngine',
     'PhabricatorFactCursor' => 'PhabricatorFactDAO',
     'PhabricatorFactDAO' => 'PhabricatorLiskDAO',
     'PhabricatorFactDaemon' => 'PhabricatorDaemon',
+    'PhabricatorFactEngine' => 'Phobject',
+    'PhabricatorFactEngineTestCase' => 'PhabricatorTestCase',
     'PhabricatorFactHomeController' => 'PhabricatorFactController',
     'PhabricatorFactLastUpdatedEngine' => 'PhabricatorFactEngine',
     'PhabricatorFactManagementAnalyzeWorkflow' => 'PhabricatorFactManagementWorkflow',
     'PhabricatorFactManagementCursorsWorkflow' => 'PhabricatorFactManagementWorkflow',
     'PhabricatorFactManagementDestroyWorkflow' => 'PhabricatorFactManagementWorkflow',
     'PhabricatorFactManagementListWorkflow' => 'PhabricatorFactManagementWorkflow',
     'PhabricatorFactManagementStatusWorkflow' => 'PhabricatorFactManagementWorkflow',
     'PhabricatorFactManagementWorkflow' => 'PhabricatorManagementWorkflow',
     'PhabricatorFactRaw' => 'PhabricatorFactDAO',
     'PhabricatorFactSimpleSpec' => 'PhabricatorFactSpec',
+    'PhabricatorFactSpec' => 'Phobject',
     'PhabricatorFactUpdateIterator' => 'PhutilBufferedIterator',
     'PhabricatorFeedApplication' => 'PhabricatorApplication',
+    'PhabricatorFeedBuilder' => 'Phobject',
     'PhabricatorFeedConfigOptions' => 'PhabricatorApplicationConfigOptions',
     'PhabricatorFeedController' => 'PhabricatorController',
     'PhabricatorFeedDAO' => 'PhabricatorLiskDAO',
     'PhabricatorFeedDetailController' => 'PhabricatorFeedController',
     'PhabricatorFeedListController' => 'PhabricatorFeedController',
     'PhabricatorFeedManagementRepublishWorkflow' => 'PhabricatorFeedManagementWorkflow',
     'PhabricatorFeedManagementWorkflow' => 'PhabricatorManagementWorkflow',
     'PhabricatorFeedPublicStreamController' => 'PhabricatorFeedController',
     'PhabricatorFeedQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'PhabricatorFeedSearchEngine' => 'PhabricatorApplicationSearchEngine',
     'PhabricatorFeedStory' => array(
+      'Phobject',
       'PhabricatorPolicyInterface',
       'PhabricatorMarkupInterface',
     ),
     'PhabricatorFeedStoryData' => 'PhabricatorFeedDAO',
     'PhabricatorFeedStoryNotification' => 'PhabricatorFeedDAO',
+    'PhabricatorFeedStoryPublisher' => 'Phobject',
     'PhabricatorFeedStoryReference' => 'PhabricatorFeedDAO',
     'PhabricatorFile' => array(
       'PhabricatorFileDAO',
       'PhabricatorApplicationTransactionInterface',
       'PhabricatorTokenReceiverInterface',
       'PhabricatorSubscribableInterface',
       'PhabricatorFlaggableInterface',
       'PhabricatorPolicyInterface',
       'PhabricatorDestructibleInterface',
     ),
+    'PhabricatorFileBundleLoader' => 'Phobject',
     'PhabricatorFileChunk' => array(
       'PhabricatorFileDAO',
       'PhabricatorPolicyInterface',
       'PhabricatorDestructibleInterface',
     ),
     'PhabricatorFileChunkIterator' => array(
       'Phobject',
       'Iterator',
     ),
     'PhabricatorFileChunkQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'PhabricatorFileCommentController' => 'PhabricatorFileController',
     'PhabricatorFileComposeController' => 'PhabricatorFileController',
     'PhabricatorFileController' => 'PhabricatorController',
     'PhabricatorFileDAO' => 'PhabricatorLiskDAO',
     'PhabricatorFileDataController' => 'PhabricatorFileController',
     'PhabricatorFileDeleteController' => 'PhabricatorFileController',
     'PhabricatorFileDropUploadController' => 'PhabricatorFileController',
     'PhabricatorFileEditController' => 'PhabricatorFileController',
     'PhabricatorFileEditor' => 'PhabricatorApplicationTransactionEditor',
     'PhabricatorFileFilePHIDType' => 'PhabricatorPHIDType',
     'PhabricatorFileHasObjectEdgeType' => 'PhabricatorEdgeType',
     'PhabricatorFileImageMacro' => array(
       'PhabricatorFileDAO',
       'PhabricatorSubscribableInterface',
       'PhabricatorApplicationTransactionInterface',
       'PhabricatorFlaggableInterface',
       'PhabricatorPolicyInterface',
     ),
     'PhabricatorFileImageTransform' => 'PhabricatorFileTransform',
     'PhabricatorFileInfoController' => 'PhabricatorFileController',
     'PhabricatorFileLinkListView' => 'AphrontView',
     'PhabricatorFileLinkView' => 'AphrontView',
     'PhabricatorFileListController' => 'PhabricatorFileController',
     'PhabricatorFileQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'PhabricatorFileSchemaSpec' => 'PhabricatorConfigSchemaSpec',
     'PhabricatorFileSearchEngine' => 'PhabricatorApplicationSearchEngine',
     'PhabricatorFileStorageBlob' => 'PhabricatorFileDAO',
     'PhabricatorFileStorageConfigurationException' => 'Exception',
+    'PhabricatorFileStorageEngine' => 'Phobject',
+    'PhabricatorFileStorageEngineTestCase' => 'PhabricatorTestCase',
     'PhabricatorFileTemporaryGarbageCollector' => 'PhabricatorGarbageCollector',
     'PhabricatorFileTestCase' => 'PhabricatorTestCase',
     'PhabricatorFileTestDataGenerator' => 'PhabricatorTestDataGenerator',
     'PhabricatorFileThumbnailTransform' => 'PhabricatorFileImageTransform',
     'PhabricatorFileTransaction' => 'PhabricatorApplicationTransaction',
     'PhabricatorFileTransactionComment' => 'PhabricatorApplicationTransactionComment',
     'PhabricatorFileTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
     'PhabricatorFileTransform' => 'Phobject',
     'PhabricatorFileTransformController' => 'PhabricatorFileController',
     'PhabricatorFileTransformListController' => 'PhabricatorFileController',
+    'PhabricatorFileTransformTestCase' => 'PhabricatorTestCase',
     'PhabricatorFileUploadController' => 'PhabricatorFileController',
     'PhabricatorFileUploadDialogController' => 'PhabricatorFileController',
     'PhabricatorFileUploadException' => 'Exception',
     'PhabricatorFileinfoSetupCheck' => 'PhabricatorSetupCheck',
     'PhabricatorFilesApplication' => 'PhabricatorApplication',
     'PhabricatorFilesApplicationStorageEnginePanel' => 'PhabricatorApplicationConfigurationPanel',
     'PhabricatorFilesConfigOptions' => 'PhabricatorApplicationConfigOptions',
     'PhabricatorFilesManagementCatWorkflow' => 'PhabricatorFilesManagementWorkflow',
     'PhabricatorFilesManagementCompactWorkflow' => 'PhabricatorFilesManagementWorkflow',
     'PhabricatorFilesManagementEnginesWorkflow' => 'PhabricatorFilesManagementWorkflow',
     'PhabricatorFilesManagementMigrateWorkflow' => 'PhabricatorFilesManagementWorkflow',
     'PhabricatorFilesManagementPurgeWorkflow' => 'PhabricatorFilesManagementWorkflow',
     'PhabricatorFilesManagementRebuildWorkflow' => 'PhabricatorFilesManagementWorkflow',
     'PhabricatorFilesManagementWorkflow' => 'PhabricatorManagementWorkflow',
     'PhabricatorFilesOutboundRequestAction' => 'PhabricatorSystemAction',
     'PhabricatorFlag' => array(
       'PhabricatorFlagDAO',
       'PhabricatorPolicyInterface',
     ),
     'PhabricatorFlagColor' => 'PhabricatorFlagConstants',
+    'PhabricatorFlagConstants' => 'Phobject',
     'PhabricatorFlagController' => 'PhabricatorController',
     'PhabricatorFlagDAO' => 'PhabricatorLiskDAO',
     'PhabricatorFlagDeleteController' => 'PhabricatorFlagController',
     'PhabricatorFlagEditController' => 'PhabricatorFlagController',
     'PhabricatorFlagListController' => 'PhabricatorFlagController',
     'PhabricatorFlagQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'PhabricatorFlagSearchEngine' => 'PhabricatorApplicationSearchEngine',
     'PhabricatorFlagSelectControl' => 'AphrontFormControl',
     'PhabricatorFlaggableInterface' => 'PhabricatorPHIDInterface',
     'PhabricatorFlagsApplication' => 'PhabricatorApplication',
     'PhabricatorFlagsUIEventListener' => 'PhabricatorEventListener',
     'PhabricatorFundApplication' => 'PhabricatorApplication',
     'PhabricatorGDSetupCheck' => 'PhabricatorSetupCheck',
     'PhabricatorGarbageCollector' => 'Phobject',
     'PhabricatorGarbageCollectorConfigOptions' => 'PhabricatorApplicationConfigOptions',
     'PhabricatorGestureUIExample' => 'PhabricatorUIExample',
     'PhabricatorGitGraphStream' => 'PhabricatorRepositoryGraphStream',
     'PhabricatorGitHubAuthProvider' => 'PhabricatorOAuth2AuthProvider',
     'PhabricatorGlobalLock' => 'PhutilLock',
     'PhabricatorGlobalUploadTargetView' => 'AphrontView',
     'PhabricatorGoogleAuthProvider' => 'PhabricatorOAuth2AuthProvider',
     'PhabricatorHandleList' => array(
       'Phobject',
       'Iterator',
       'ArrayAccess',
       'Countable',
     ),
+    'PhabricatorHandleObjectSelectorDataView' => 'Phobject',
     'PhabricatorHandlePool' => 'Phobject',
     'PhabricatorHandlePoolTestCase' => 'PhabricatorTestCase',
     'PhabricatorHandleQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'PhabricatorHarbormasterApplication' => 'PhabricatorApplication',
     'PhabricatorHarbormasterConfigOptions' => 'PhabricatorApplicationConfigOptions',
     'PhabricatorHash' => 'Phobject',
     'PhabricatorHashTestCase' => 'PhabricatorTestCase',
     'PhabricatorHelpApplication' => 'PhabricatorApplication',
     'PhabricatorHelpController' => 'PhabricatorController',
     'PhabricatorHelpDocumentationController' => 'PhabricatorHelpController',
     'PhabricatorHelpEditorProtocolController' => 'PhabricatorHelpController',
     'PhabricatorHelpKeyboardShortcutController' => 'PhabricatorHelpController',
     'PhabricatorHeraldApplication' => 'PhabricatorApplication',
     'PhabricatorHomeApplication' => 'PhabricatorApplication',
     'PhabricatorHomeController' => 'PhabricatorController',
     'PhabricatorHomeMainController' => 'PhabricatorHomeController',
     'PhabricatorHomePreferencesSettingsPanel' => 'PhabricatorSettingsPanel',
     'PhabricatorHomeQuickCreateController' => 'PhabricatorHomeController',
     'PhabricatorHovercardUIExample' => 'PhabricatorUIExample',
     'PhabricatorHovercardView' => 'AphrontView',
     'PhabricatorHunksManagementMigrateWorkflow' => 'PhabricatorHunksManagementWorkflow',
     'PhabricatorHunksManagementWorkflow' => 'PhabricatorManagementWorkflow',
     'PhabricatorIRCProtocolAdapter' => 'PhabricatorProtocolAdapter',
     'PhabricatorIconRemarkupRule' => 'PhutilRemarkupRule',
     'PhabricatorImageMacroRemarkupRule' => 'PhutilRemarkupRule',
+    'PhabricatorImageTransformer' => 'Phobject',
     'PhabricatorImagemagickSetupCheck' => 'PhabricatorSetupCheck',
     'PhabricatorInfrastructureTestCase' => 'PhabricatorTestCase',
     'PhabricatorInlineCommentController' => 'PhabricatorController',
     'PhabricatorInlineCommentInterface' => 'PhabricatorMarkupInterface',
     'PhabricatorInlineCommentPreviewController' => 'PhabricatorController',
     'PhabricatorInlineSummaryView' => 'AphrontView',
     'PhabricatorInternationalizationManagementExtractWorkflow' => 'PhabricatorInternationalizationManagementWorkflow',
     'PhabricatorInternationalizationManagementWorkflow' => 'PhabricatorManagementWorkflow',
     'PhabricatorInvalidConfigSetupCheck' => 'PhabricatorSetupCheck',
     'PhabricatorIteratedMD5PasswordHasher' => 'PhabricatorPasswordHasher',
+    'PhabricatorIteratedMD5PasswordHasherTestCase' => 'PhabricatorTestCase',
     'PhabricatorJIRAAuthProvider' => 'PhabricatorOAuth1AuthProvider',
     'PhabricatorJavelinLinter' => 'ArcanistLinter',
     'PhabricatorJiraIssueHasObjectEdgeType' => 'PhabricatorEdgeType',
+    'PhabricatorJumpNavHandler' => 'Phobject',
     'PhabricatorKeyValueDatabaseCache' => 'PhutilKeyValueCache',
     'PhabricatorLDAPAuthProvider' => 'PhabricatorAuthProvider',
     'PhabricatorLegalpadApplication' => 'PhabricatorApplication',
     'PhabricatorLegalpadConfigOptions' => 'PhabricatorApplicationConfigOptions',
     'PhabricatorLegalpadDocumentPHIDType' => 'PhabricatorPHIDType',
     'PhabricatorLegalpadSignaturePolicyRule' => 'PhabricatorPolicyRule',
     'PhabricatorLibraryTestCase' => 'PhutilLibraryTestCase',
+    'PhabricatorLipsumArtist' => 'Phobject',
     'PhabricatorLipsumGenerateWorkflow' => 'PhabricatorLipsumManagementWorkflow',
     'PhabricatorLipsumManagementWorkflow' => 'PhabricatorManagementWorkflow',
     'PhabricatorLipsumMondrianArtist' => 'PhabricatorLipsumArtist',
     'PhabricatorLiskDAO' => 'LiskDAO',
+    'PhabricatorLiskSerializer' => 'Phobject',
     'PhabricatorListFilterUIExample' => 'PhabricatorUIExample',
     'PhabricatorLocalDiskFileStorageEngine' => 'PhabricatorFileStorageEngine',
     'PhabricatorLocalTimeTestCase' => 'PhabricatorTestCase',
     'PhabricatorLocaleScopeGuard' => 'Phobject',
     'PhabricatorLocaleScopeGuardTestCase' => 'PhabricatorTestCase',
     'PhabricatorLogTriggerAction' => 'PhabricatorTriggerAction',
     'PhabricatorLogoutController' => 'PhabricatorAuthController',
     'PhabricatorLunarPhasePolicyRule' => 'PhabricatorPolicyRule',
     'PhabricatorMacroApplication' => 'PhabricatorApplication',
     'PhabricatorMacroAudioController' => 'PhabricatorMacroController',
     'PhabricatorMacroCommentController' => 'PhabricatorMacroController',
     'PhabricatorMacroConfigOptions' => 'PhabricatorApplicationConfigOptions',
     'PhabricatorMacroController' => 'PhabricatorController',
     'PhabricatorMacroDatasource' => 'PhabricatorTypeaheadDatasource',
     'PhabricatorMacroDisableController' => 'PhabricatorMacroController',
     'PhabricatorMacroEditController' => 'PhabricatorMacroController',
     'PhabricatorMacroEditor' => 'PhabricatorApplicationTransactionEditor',
     'PhabricatorMacroListController' => 'PhabricatorMacroController',
     'PhabricatorMacroMacroPHIDType' => 'PhabricatorPHIDType',
     'PhabricatorMacroMailReceiver' => 'PhabricatorObjectMailReceiver',
     'PhabricatorMacroManageCapability' => 'PhabricatorPolicyCapability',
     'PhabricatorMacroMemeController' => 'PhabricatorMacroController',
     'PhabricatorMacroMemeDialogController' => 'PhabricatorMacroController',
     'PhabricatorMacroQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'PhabricatorMacroReplyHandler' => 'PhabricatorApplicationTransactionReplyHandler',
     'PhabricatorMacroSearchEngine' => 'PhabricatorApplicationSearchEngine',
     'PhabricatorMacroTransaction' => 'PhabricatorApplicationTransaction',
     'PhabricatorMacroTransactionComment' => 'PhabricatorApplicationTransactionComment',
     'PhabricatorMacroTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
     'PhabricatorMacroViewController' => 'PhabricatorMacroController',
+    'PhabricatorMailImplementationAdapter' => 'Phobject',
     'PhabricatorMailImplementationAmazonSESAdapter' => 'PhabricatorMailImplementationPHPMailerLiteAdapter',
     'PhabricatorMailImplementationMailgunAdapter' => 'PhabricatorMailImplementationAdapter',
     'PhabricatorMailImplementationPHPMailerAdapter' => 'PhabricatorMailImplementationAdapter',
     'PhabricatorMailImplementationPHPMailerLiteAdapter' => 'PhabricatorMailImplementationAdapter',
     'PhabricatorMailImplementationSendGridAdapter' => 'PhabricatorMailImplementationAdapter',
     'PhabricatorMailImplementationTestAdapter' => 'PhabricatorMailImplementationAdapter',
     'PhabricatorMailManagementListInboundWorkflow' => 'PhabricatorMailManagementWorkflow',
     'PhabricatorMailManagementListOutboundWorkflow' => 'PhabricatorMailManagementWorkflow',
     'PhabricatorMailManagementReceiveTestWorkflow' => 'PhabricatorMailManagementWorkflow',
     'PhabricatorMailManagementResendWorkflow' => 'PhabricatorMailManagementWorkflow',
     'PhabricatorMailManagementSendTestWorkflow' => 'PhabricatorMailManagementWorkflow',
     'PhabricatorMailManagementShowInboundWorkflow' => 'PhabricatorMailManagementWorkflow',
     'PhabricatorMailManagementShowOutboundWorkflow' => 'PhabricatorMailManagementWorkflow',
     'PhabricatorMailManagementWorkflow' => 'PhabricatorManagementWorkflow',
+    'PhabricatorMailReceiver' => 'Phobject',
     'PhabricatorMailReceiverTestCase' => 'PhabricatorTestCase',
+    'PhabricatorMailReplyHandler' => 'Phobject',
     'PhabricatorMailSetupCheck' => 'PhabricatorSetupCheck',
     'PhabricatorMailTarget' => 'Phobject',
     'PhabricatorMailgunConfigOptions' => 'PhabricatorApplicationConfigOptions',
     'PhabricatorMainMenuSearchView' => 'AphrontView',
     'PhabricatorMainMenuView' => 'AphrontView',
     'PhabricatorManagementWorkflow' => 'PhutilArgumentWorkflow',
     'PhabricatorManiphestApplication' => 'PhabricatorApplication',
     'PhabricatorManiphestConfigOptions' => 'PhabricatorApplicationConfigOptions',
     'PhabricatorManiphestTaskTestDataGenerator' => 'PhabricatorTestDataGenerator',
     'PhabricatorMarkupCache' => 'PhabricatorCacheDAO',
-    'PhabricatorMarkupOneOff' => 'PhabricatorMarkupInterface',
+    'PhabricatorMarkupEngine' => 'Phobject',
+    'PhabricatorMarkupOneOff' => array(
+      'Phobject',
+      'PhabricatorMarkupInterface',
+    ),
     'PhabricatorMarkupPreviewController' => 'PhabricatorController',
     'PhabricatorMemeRemarkupRule' => 'PhutilRemarkupRule',
     'PhabricatorMentionRemarkupRule' => 'PhutilRemarkupRule',
     'PhabricatorMercurialGraphStream' => 'PhabricatorRepositoryGraphStream',
+    'PhabricatorMetaMTAActor' => 'Phobject',
     'PhabricatorMetaMTAActorQuery' => 'PhabricatorQuery',
     'PhabricatorMetaMTAApplication' => 'PhabricatorApplication',
     'PhabricatorMetaMTAApplicationEmail' => array(
       'PhabricatorMetaMTADAO',
       'PhabricatorPolicyInterface',
+      'PhabricatorApplicationTransactionInterface',
+      'PhabricatorDestructibleInterface',
+      'PhabricatorSpacesInterface',
     ),
     'PhabricatorMetaMTAApplicationEmailDatasource' => 'PhabricatorTypeaheadDatasource',
+    'PhabricatorMetaMTAApplicationEmailEditor' => 'PhabricatorApplicationTransactionEditor',
     'PhabricatorMetaMTAApplicationEmailPHIDType' => 'PhabricatorPHIDType',
     'PhabricatorMetaMTAApplicationEmailPanel' => 'PhabricatorApplicationConfigurationPanel',
     'PhabricatorMetaMTAApplicationEmailQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
+    'PhabricatorMetaMTAApplicationEmailTransaction' => 'PhabricatorApplicationTransaction',
+    'PhabricatorMetaMTAApplicationEmailTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
+    'PhabricatorMetaMTAAttachment' => 'Phobject',
     'PhabricatorMetaMTAConfigOptions' => 'PhabricatorApplicationConfigOptions',
     'PhabricatorMetaMTAController' => 'PhabricatorController',
     'PhabricatorMetaMTADAO' => 'PhabricatorLiskDAO',
+    'PhabricatorMetaMTAEmailBodyParser' => 'Phobject',
     'PhabricatorMetaMTAEmailBodyParserTestCase' => 'PhabricatorTestCase',
     'PhabricatorMetaMTAErrorMailAction' => 'PhabricatorSystemAction',
     'PhabricatorMetaMTAMail' => 'PhabricatorMetaMTADAO',
+    'PhabricatorMetaMTAMailBody' => 'Phobject',
     'PhabricatorMetaMTAMailBodyTestCase' => 'PhabricatorTestCase',
+    'PhabricatorMetaMTAMailSection' => 'Phobject',
     'PhabricatorMetaMTAMailTestCase' => 'PhabricatorTestCase',
     'PhabricatorMetaMTAMailableDatasource' => 'PhabricatorTypeaheadCompositeDatasource',
     'PhabricatorMetaMTAMailableFunctionDatasource' => 'PhabricatorTypeaheadCompositeDatasource',
     'PhabricatorMetaMTAMailgunReceiveController' => 'PhabricatorMetaMTAController',
     'PhabricatorMetaMTAMailingList' => 'PhabricatorMetaMTADAO',
     'PhabricatorMetaMTAMemberQuery' => 'PhabricatorQuery',
     'PhabricatorMetaMTAPermanentFailureException' => 'Exception',
     'PhabricatorMetaMTAReceivedMail' => 'PhabricatorMetaMTADAO',
     'PhabricatorMetaMTAReceivedMailProcessingException' => 'Exception',
     'PhabricatorMetaMTAReceivedMailTestCase' => 'PhabricatorTestCase',
     'PhabricatorMetaMTASchemaSpec' => 'PhabricatorConfigSchemaSpec',
     'PhabricatorMetaMTASendGridReceiveController' => 'PhabricatorMetaMTAController',
     'PhabricatorMetaMTAWorker' => 'PhabricatorWorker',
     'PhabricatorMetronomicTriggerClock' => 'PhabricatorTriggerClock',
     'PhabricatorMultiColumnUIExample' => 'PhabricatorUIExample',
     'PhabricatorMultiFactorSettingsPanel' => 'PhabricatorSettingsPanel',
     'PhabricatorMultimeterApplication' => 'PhabricatorApplication',
     'PhabricatorMustVerifyEmailController' => 'PhabricatorAuthController',
     'PhabricatorMySQLConfigOptions' => 'PhabricatorApplicationConfigOptions',
     'PhabricatorMySQLFileStorageEngine' => 'PhabricatorFileStorageEngine',
     'PhabricatorMySQLSearchEngine' => 'PhabricatorSearchEngine',
     'PhabricatorMySQLSetupCheck' => 'PhabricatorSetupCheck',
     'PhabricatorNamedQuery' => array(
       'PhabricatorSearchDAO',
       'PhabricatorPolicyInterface',
     ),
     'PhabricatorNamedQueryQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'PhabricatorNavigationRemarkupRule' => 'PhutilRemarkupRule',
     'PhabricatorNeverTriggerClock' => 'PhabricatorTriggerClock',
     'PhabricatorNotificationAdHocFeedStory' => 'PhabricatorFeedStory',
+    'PhabricatorNotificationBuilder' => 'Phobject',
     'PhabricatorNotificationClearController' => 'PhabricatorNotificationController',
+    'PhabricatorNotificationClient' => 'Phobject',
     'PhabricatorNotificationConfigOptions' => 'PhabricatorApplicationConfigOptions',
     'PhabricatorNotificationController' => 'PhabricatorController',
     'PhabricatorNotificationIndividualController' => 'PhabricatorNotificationController',
     'PhabricatorNotificationListController' => 'PhabricatorNotificationController',
     'PhabricatorNotificationPanelController' => 'PhabricatorNotificationController',
     'PhabricatorNotificationQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'PhabricatorNotificationSearchEngine' => 'PhabricatorApplicationSearchEngine',
     'PhabricatorNotificationStatusController' => 'PhabricatorNotificationController',
     'PhabricatorNotificationStatusView' => 'AphrontTagView',
     'PhabricatorNotificationTestController' => 'PhabricatorNotificationController',
     'PhabricatorNotificationUIExample' => 'PhabricatorUIExample',
     'PhabricatorNotificationsApplication' => 'PhabricatorApplication',
     'PhabricatorNuanceApplication' => 'PhabricatorApplication',
     'PhabricatorOAuth1AuthProvider' => 'PhabricatorOAuthAuthProvider',
     'PhabricatorOAuth2AuthProvider' => 'PhabricatorOAuthAuthProvider',
     'PhabricatorOAuthAuthProvider' => 'PhabricatorAuthProvider',
     'PhabricatorOAuthClientAuthorization' => array(
       'PhabricatorOAuthServerDAO',
       'PhabricatorPolicyInterface',
     ),
     'PhabricatorOAuthClientAuthorizationQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'PhabricatorOAuthClientController' => 'PhabricatorOAuthServerController',
     'PhabricatorOAuthClientDeleteController' => 'PhabricatorOAuthClientController',
     'PhabricatorOAuthClientEditController' => 'PhabricatorOAuthClientController',
     'PhabricatorOAuthClientListController' => 'PhabricatorOAuthClientController',
     'PhabricatorOAuthClientSecretController' => 'PhabricatorOAuthClientController',
     'PhabricatorOAuthClientViewController' => 'PhabricatorOAuthClientController',
     'PhabricatorOAuthResponse' => 'AphrontResponse',
+    'PhabricatorOAuthServer' => 'Phobject',
     'PhabricatorOAuthServerAccessToken' => 'PhabricatorOAuthServerDAO',
     'PhabricatorOAuthServerApplication' => 'PhabricatorApplication',
     'PhabricatorOAuthServerAuthController' => 'PhabricatorAuthController',
     'PhabricatorOAuthServerAuthorizationCode' => 'PhabricatorOAuthServerDAO',
     'PhabricatorOAuthServerAuthorizationsSettingsPanel' => 'PhabricatorSettingsPanel',
     'PhabricatorOAuthServerClient' => array(
       'PhabricatorOAuthServerDAO',
       'PhabricatorPolicyInterface',
       'PhabricatorDestructibleInterface',
     ),
     'PhabricatorOAuthServerClientAuthorizationPHIDType' => 'PhabricatorPHIDType',
     'PhabricatorOAuthServerClientPHIDType' => 'PhabricatorPHIDType',
     'PhabricatorOAuthServerClientQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'PhabricatorOAuthServerClientSearchEngine' => 'PhabricatorApplicationSearchEngine',
     'PhabricatorOAuthServerController' => 'PhabricatorController',
     'PhabricatorOAuthServerCreateClientsCapability' => 'PhabricatorPolicyCapability',
     'PhabricatorOAuthServerDAO' => 'PhabricatorLiskDAO',
+    'PhabricatorOAuthServerScope' => 'Phobject',
     'PhabricatorOAuthServerTestCase' => 'PhabricatorTestCase',
     'PhabricatorOAuthServerTestController' => 'PhabricatorOAuthServerController',
     'PhabricatorOAuthServerTokenController' => 'PhabricatorAuthController',
-    'PhabricatorObjectHandle' => 'PhabricatorPolicyInterface',
+    'PhabricatorObjectHandle' => array(
+      'Phobject',
+      'PhabricatorPolicyInterface',
+    ),
     'PhabricatorObjectHasAsanaSubtaskEdgeType' => 'PhabricatorEdgeType',
     'PhabricatorObjectHasAsanaTaskEdgeType' => 'PhabricatorEdgeType',
     'PhabricatorObjectHasContributorEdgeType' => 'PhabricatorEdgeType',
     'PhabricatorObjectHasFileEdgeType' => 'PhabricatorEdgeType',
     'PhabricatorObjectHasJiraIssueEdgeType' => 'PhabricatorEdgeType',
     'PhabricatorObjectHasSubscriberEdgeType' => 'PhabricatorEdgeType',
     'PhabricatorObjectHasUnsubscriberEdgeType' => 'PhabricatorEdgeType',
     'PhabricatorObjectHasWatcherEdgeType' => 'PhabricatorEdgeType',
+    'PhabricatorObjectListQuery' => 'Phobject',
     'PhabricatorObjectListQueryTestCase' => 'PhabricatorTestCase',
     'PhabricatorObjectMailReceiver' => 'PhabricatorMailReceiver',
     'PhabricatorObjectMailReceiverTestCase' => 'PhabricatorTestCase',
     'PhabricatorObjectMentionedByObjectEdgeType' => 'PhabricatorEdgeType',
     'PhabricatorObjectMentionsObjectEdgeType' => 'PhabricatorEdgeType',
     'PhabricatorObjectQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'PhabricatorObjectRemarkupRule' => 'PhutilRemarkupRule',
+    'PhabricatorObjectSelectorDialog' => 'Phobject',
     'PhabricatorObjectUsesCredentialsEdgeType' => 'PhabricatorEdgeType',
     'PhabricatorOffsetPagedQuery' => 'PhabricatorQuery',
     'PhabricatorOneTimeTriggerClock' => 'PhabricatorTriggerClock',
     'PhabricatorOpcodeCacheSpec' => 'PhabricatorCacheSpec',
+    'PhabricatorOwnerPathQuery' => 'Phobject',
     'PhabricatorOwnersApplication' => 'PhabricatorApplication',
     'PhabricatorOwnersConfigOptions' => 'PhabricatorApplicationConfigOptions',
     'PhabricatorOwnersController' => 'PhabricatorController',
     'PhabricatorOwnersDAO' => 'PhabricatorLiskDAO',
     'PhabricatorOwnersDetailController' => 'PhabricatorOwnersController',
     'PhabricatorOwnersEditController' => 'PhabricatorOwnersController',
     'PhabricatorOwnersListController' => 'PhabricatorOwnersController',
     'PhabricatorOwnersOwner' => 'PhabricatorOwnersDAO',
     'PhabricatorOwnersPackage' => array(
       'PhabricatorOwnersDAO',
       'PhabricatorPolicyInterface',
       'PhabricatorApplicationTransactionInterface',
     ),
     'PhabricatorOwnersPackageDatasource' => 'PhabricatorTypeaheadDatasource',
     'PhabricatorOwnersPackagePHIDType' => 'PhabricatorPHIDType',
     'PhabricatorOwnersPackageQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'PhabricatorOwnersPackageSearchEngine' => 'PhabricatorApplicationSearchEngine',
     'PhabricatorOwnersPackageTestCase' => 'PhabricatorTestCase',
     'PhabricatorOwnersPackageTransaction' => 'PhabricatorApplicationTransaction',
     'PhabricatorOwnersPackageTransactionEditor' => 'PhabricatorApplicationTransactionEditor',
     'PhabricatorOwnersPackageTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
     'PhabricatorOwnersPath' => 'PhabricatorOwnersDAO',
     'PhabricatorOwnersPathsController' => 'PhabricatorOwnersController',
     'PhabricatorPHDConfigOptions' => 'PhabricatorApplicationConfigOptions',
+    'PhabricatorPHID' => 'Phobject',
+    'PhabricatorPHIDConstants' => 'Phobject',
+    'PhabricatorPHIDType' => 'Phobject',
+    'PhabricatorPHIDTypeTestCase' => 'PhutilTestCase',
     'PhabricatorPHPASTApplication' => 'PhabricatorApplication',
     'PhabricatorPHPConfigSetupCheck' => 'PhabricatorSetupCheck',
     'PhabricatorPHPMailerConfigOptions' => 'PhabricatorApplicationConfigOptions',
     'PhabricatorPagedFormUIExample' => 'PhabricatorUIExample',
     'PhabricatorPagerUIExample' => 'PhabricatorUIExample',
     'PhabricatorPassphraseApplication' => 'PhabricatorApplication',
     'PhabricatorPasswordAuthProvider' => 'PhabricatorAuthProvider',
     'PhabricatorPasswordHasher' => 'Phobject',
     'PhabricatorPasswordHasherTestCase' => 'PhabricatorTestCase',
     'PhabricatorPasswordHasherUnavailableException' => 'Exception',
     'PhabricatorPasswordSettingsPanel' => 'PhabricatorSettingsPanel',
     'PhabricatorPaste' => array(
       'PhabricatorPasteDAO',
       'PhabricatorSubscribableInterface',
       'PhabricatorTokenReceiverInterface',
       'PhabricatorFlaggableInterface',
       'PhabricatorMentionableInterface',
       'PhabricatorPolicyInterface',
       'PhabricatorProjectInterface',
       'PhabricatorDestructibleInterface',
       'PhabricatorApplicationTransactionInterface',
       'PhabricatorSpacesInterface',
     ),
     'PhabricatorPasteApplication' => 'PhabricatorApplication',
     'PhabricatorPasteCommentController' => 'PhabricatorPasteController',
     'PhabricatorPasteConfigOptions' => 'PhabricatorApplicationConfigOptions',
     'PhabricatorPasteController' => 'PhabricatorController',
     'PhabricatorPasteDAO' => 'PhabricatorLiskDAO',
     'PhabricatorPasteEditController' => 'PhabricatorPasteController',
     'PhabricatorPasteEditor' => 'PhabricatorApplicationTransactionEditor',
     'PhabricatorPasteListController' => 'PhabricatorPasteController',
     'PhabricatorPastePastePHIDType' => 'PhabricatorPHIDType',
     'PhabricatorPasteQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'PhabricatorPasteRemarkupRule' => 'PhabricatorObjectRemarkupRule',
     'PhabricatorPasteSchemaSpec' => 'PhabricatorConfigSchemaSpec',
     'PhabricatorPasteSearchEngine' => 'PhabricatorApplicationSearchEngine',
     'PhabricatorPasteTestDataGenerator' => 'PhabricatorTestDataGenerator',
     'PhabricatorPasteTransaction' => 'PhabricatorApplicationTransaction',
     'PhabricatorPasteTransactionComment' => 'PhabricatorApplicationTransactionComment',
     'PhabricatorPasteTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
     'PhabricatorPasteViewController' => 'PhabricatorPasteController',
     'PhabricatorPathSetupCheck' => 'PhabricatorSetupCheck',
     'PhabricatorPeopleAnyOwnerDatasource' => 'PhabricatorTypeaheadDatasource',
     'PhabricatorPeopleApplication' => 'PhabricatorApplication',
     'PhabricatorPeopleApproveController' => 'PhabricatorPeopleController',
     'PhabricatorPeopleCalendarController' => 'PhabricatorPeopleController',
     'PhabricatorPeopleController' => 'PhabricatorController',
     'PhabricatorPeopleCreateController' => 'PhabricatorPeopleController',
     'PhabricatorPeopleDatasource' => 'PhabricatorTypeaheadDatasource',
     'PhabricatorPeopleDeleteController' => 'PhabricatorPeopleController',
     'PhabricatorPeopleDisableController' => 'PhabricatorPeopleController',
     'PhabricatorPeopleEmpowerController' => 'PhabricatorPeopleController',
     'PhabricatorPeopleExternalPHIDType' => 'PhabricatorPHIDType',
     'PhabricatorPeopleHovercardEventListener' => 'PhabricatorEventListener',
     'PhabricatorPeopleInviteController' => 'PhabricatorPeopleController',
     'PhabricatorPeopleInviteListController' => 'PhabricatorPeopleInviteController',
     'PhabricatorPeopleInviteSendController' => 'PhabricatorPeopleInviteController',
     'PhabricatorPeopleLdapController' => 'PhabricatorPeopleController',
     'PhabricatorPeopleListController' => 'PhabricatorPeopleController',
     'PhabricatorPeopleLogQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'PhabricatorPeopleLogSearchEngine' => 'PhabricatorApplicationSearchEngine',
     'PhabricatorPeopleLogsController' => 'PhabricatorPeopleController',
     'PhabricatorPeopleNewController' => 'PhabricatorPeopleController',
     'PhabricatorPeopleNoOwnerDatasource' => 'PhabricatorTypeaheadDatasource',
     'PhabricatorPeopleOwnerDatasource' => 'PhabricatorTypeaheadCompositeDatasource',
     'PhabricatorPeopleProfileController' => 'PhabricatorPeopleController',
     'PhabricatorPeopleProfileEditController' => 'PhabricatorPeopleController',
     'PhabricatorPeopleProfilePictureController' => 'PhabricatorPeopleController',
     'PhabricatorPeopleQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'PhabricatorPeopleRenameController' => 'PhabricatorPeopleController',
     'PhabricatorPeopleSearchEngine' => 'PhabricatorApplicationSearchEngine',
     'PhabricatorPeopleTestDataGenerator' => 'PhabricatorTestDataGenerator',
     'PhabricatorPeopleTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
     'PhabricatorPeopleUserFunctionDatasource' => 'PhabricatorTypeaheadCompositeDatasource',
     'PhabricatorPeopleUserPHIDType' => 'PhabricatorPHIDType',
     'PhabricatorPeopleWelcomeController' => 'PhabricatorPeopleController',
     'PhabricatorPersonaAuthProvider' => 'PhabricatorAuthProvider',
     'PhabricatorPhabricatorAuthProvider' => 'PhabricatorOAuth2AuthProvider',
     'PhabricatorPhameApplication' => 'PhabricatorApplication',
     'PhabricatorPhameBlogPHIDType' => 'PhabricatorPHIDType',
     'PhabricatorPhameConfigOptions' => 'PhabricatorApplicationConfigOptions',
     'PhabricatorPhamePostPHIDType' => 'PhabricatorPHIDType',
     'PhabricatorPhluxApplication' => 'PhabricatorApplication',
     'PhabricatorPholioApplication' => 'PhabricatorApplication',
     'PhabricatorPholioConfigOptions' => 'PhabricatorApplicationConfigOptions',
     'PhabricatorPholioMockTestDataGenerator' => 'PhabricatorTestDataGenerator',
     'PhabricatorPhortuneApplication' => 'PhabricatorApplication',
     'PhabricatorPhortuneManagementInvoiceWorkflow' => 'PhabricatorPhortuneManagementWorkflow',
     'PhabricatorPhortuneManagementWorkflow' => 'PhabricatorManagementWorkflow',
     'PhabricatorPhragmentApplication' => 'PhabricatorApplication',
     'PhabricatorPhrequentApplication' => 'PhabricatorApplication',
     'PhabricatorPhrequentConfigOptions' => 'PhabricatorApplicationConfigOptions',
     'PhabricatorPhrictionApplication' => 'PhabricatorApplication',
     'PhabricatorPhrictionConfigOptions' => 'PhabricatorApplicationConfigOptions',
     'PhabricatorPolicies' => 'PhabricatorPolicyConstants',
     'PhabricatorPolicy' => array(
       'PhabricatorPolicyDAO',
       'PhabricatorPolicyInterface',
       'PhabricatorDestructibleInterface',
     ),
     'PhabricatorPolicyApplication' => 'PhabricatorApplication',
     'PhabricatorPolicyAwareQuery' => 'PhabricatorOffsetPagedQuery',
     'PhabricatorPolicyAwareTestQuery' => 'PhabricatorPolicyAwareQuery',
     'PhabricatorPolicyCanEditCapability' => 'PhabricatorPolicyCapability',
     'PhabricatorPolicyCanJoinCapability' => 'PhabricatorPolicyCapability',
     'PhabricatorPolicyCanViewCapability' => 'PhabricatorPolicyCapability',
     'PhabricatorPolicyCapability' => 'Phobject',
+    'PhabricatorPolicyCapabilityTestCase' => 'PhabricatorTestCase',
     'PhabricatorPolicyConfigOptions' => 'PhabricatorApplicationConfigOptions',
+    'PhabricatorPolicyConstants' => 'Phobject',
     'PhabricatorPolicyController' => 'PhabricatorController',
     'PhabricatorPolicyDAO' => 'PhabricatorLiskDAO',
     'PhabricatorPolicyDataTestCase' => 'PhabricatorTestCase',
     'PhabricatorPolicyEditController' => 'PhabricatorPolicyController',
     'PhabricatorPolicyException' => 'Exception',
     'PhabricatorPolicyExplainController' => 'PhabricatorPolicyController',
+    'PhabricatorPolicyFilter' => 'Phobject',
     'PhabricatorPolicyInterface' => 'PhabricatorPHIDInterface',
     'PhabricatorPolicyManagementShowWorkflow' => 'PhabricatorPolicyManagementWorkflow',
     'PhabricatorPolicyManagementUnlockWorkflow' => 'PhabricatorPolicyManagementWorkflow',
     'PhabricatorPolicyManagementWorkflow' => 'PhabricatorManagementWorkflow',
     'PhabricatorPolicyPHIDTypePolicy' => 'PhabricatorPHIDType',
     'PhabricatorPolicyQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
+    'PhabricatorPolicyRule' => 'Phobject',
     'PhabricatorPolicyTestCase' => 'PhabricatorTestCase',
     'PhabricatorPolicyTestObject' => array(
+      'Phobject',
       'PhabricatorPolicyInterface',
       'PhabricatorExtendedPolicyInterface',
     ),
     'PhabricatorPolicyType' => 'PhabricatorPolicyConstants',
     'PhabricatorPonderApplication' => 'PhabricatorApplication',
     'PhabricatorProject' => array(
       'PhabricatorProjectDAO',
       'PhabricatorApplicationTransactionInterface',
       'PhabricatorFlaggableInterface',
       'PhabricatorPolicyInterface',
       'PhabricatorSubscribableInterface',
       'PhabricatorCustomFieldInterface',
       'PhabricatorDestructibleInterface',
     ),
     'PhabricatorProjectApplication' => 'PhabricatorApplication',
     'PhabricatorProjectArchiveController' => 'PhabricatorProjectController',
     'PhabricatorProjectBoardController' => 'PhabricatorProjectController',
     'PhabricatorProjectBoardImportController' => 'PhabricatorProjectBoardController',
     'PhabricatorProjectBoardReorderController' => 'PhabricatorProjectBoardController',
     'PhabricatorProjectBoardViewController' => 'PhabricatorProjectBoardController',
     'PhabricatorProjectColumn' => array(
       'PhabricatorProjectDAO',
       'PhabricatorApplicationTransactionInterface',
       'PhabricatorPolicyInterface',
       'PhabricatorDestructibleInterface',
     ),
     'PhabricatorProjectColumnDetailController' => 'PhabricatorProjectBoardController',
     'PhabricatorProjectColumnEditController' => 'PhabricatorProjectBoardController',
     'PhabricatorProjectColumnHideController' => 'PhabricatorProjectBoardController',
     'PhabricatorProjectColumnPHIDType' => 'PhabricatorPHIDType',
     'PhabricatorProjectColumnPosition' => array(
       'PhabricatorProjectDAO',
       'PhabricatorPolicyInterface',
     ),
     'PhabricatorProjectColumnPositionQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'PhabricatorProjectColumnQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'PhabricatorProjectColumnTransaction' => 'PhabricatorApplicationTransaction',
     'PhabricatorProjectColumnTransactionEditor' => 'PhabricatorApplicationTransactionEditor',
     'PhabricatorProjectColumnTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
     'PhabricatorProjectConfigOptions' => 'PhabricatorApplicationConfigOptions',
     'PhabricatorProjectConfiguredCustomField' => array(
       'PhabricatorProjectStandardCustomField',
       'PhabricatorStandardCustomFieldInterface',
     ),
     'PhabricatorProjectController' => 'PhabricatorController',
     'PhabricatorProjectCustomField' => 'PhabricatorCustomField',
     'PhabricatorProjectCustomFieldNumericIndex' => 'PhabricatorCustomFieldNumericIndexStorage',
     'PhabricatorProjectCustomFieldStorage' => 'PhabricatorCustomFieldStorage',
     'PhabricatorProjectCustomFieldStringIndex' => 'PhabricatorCustomFieldStringIndexStorage',
     'PhabricatorProjectDAO' => 'PhabricatorLiskDAO',
     'PhabricatorProjectDatasource' => 'PhabricatorTypeaheadDatasource',
     'PhabricatorProjectDescriptionField' => 'PhabricatorProjectStandardCustomField',
     'PhabricatorProjectEditDetailsController' => 'PhabricatorProjectController',
     'PhabricatorProjectEditIconController' => 'PhabricatorProjectController',
     'PhabricatorProjectEditPictureController' => 'PhabricatorProjectController',
     'PhabricatorProjectEditorTestCase' => 'PhabricatorTestCase',
     'PhabricatorProjectFeedController' => 'PhabricatorProjectController',
     'PhabricatorProjectIcon' => 'Phobject',
     'PhabricatorProjectListController' => 'PhabricatorProjectController',
     'PhabricatorProjectLogicalAndDatasource' => 'PhabricatorTypeaheadCompositeDatasource',
     'PhabricatorProjectLogicalDatasource' => 'PhabricatorTypeaheadCompositeDatasource',
     'PhabricatorProjectLogicalOrNotDatasource' => 'PhabricatorTypeaheadCompositeDatasource',
     'PhabricatorProjectLogicalUserDatasource' => 'PhabricatorTypeaheadCompositeDatasource',
     'PhabricatorProjectLogicalViewerDatasource' => 'PhabricatorTypeaheadDatasource',
     'PhabricatorProjectMemberOfProjectEdgeType' => 'PhabricatorEdgeType',
     'PhabricatorProjectMembersDatasource' => 'PhabricatorTypeaheadCompositeDatasource',
     'PhabricatorProjectMembersEditController' => 'PhabricatorProjectController',
     'PhabricatorProjectMembersRemoveController' => 'PhabricatorProjectController',
     'PhabricatorProjectMoveController' => 'PhabricatorProjectController',
     'PhabricatorProjectNoProjectsDatasource' => 'PhabricatorTypeaheadDatasource',
     'PhabricatorProjectObjectHasProjectEdgeType' => 'PhabricatorEdgeType',
     'PhabricatorProjectOrUserDatasource' => 'PhabricatorTypeaheadCompositeDatasource',
     'PhabricatorProjectProfileController' => 'PhabricatorProjectController',
     'PhabricatorProjectProjectHasMemberEdgeType' => 'PhabricatorEdgeType',
     'PhabricatorProjectProjectHasObjectEdgeType' => 'PhabricatorEdgeType',
     'PhabricatorProjectProjectPHIDType' => 'PhabricatorPHIDType',
     'PhabricatorProjectQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'PhabricatorProjectSchemaSpec' => 'PhabricatorConfigSchemaSpec',
     'PhabricatorProjectSearchEngine' => 'PhabricatorApplicationSearchEngine',
     'PhabricatorProjectSearchIndexer' => 'PhabricatorSearchDocumentIndexer',
     'PhabricatorProjectSlug' => 'PhabricatorProjectDAO',
     'PhabricatorProjectStandardCustomField' => array(
       'PhabricatorProjectCustomField',
       'PhabricatorStandardCustomFieldInterface',
     ),
+    'PhabricatorProjectStatus' => 'Phobject',
     'PhabricatorProjectTestDataGenerator' => 'PhabricatorTestDataGenerator',
     'PhabricatorProjectTransaction' => 'PhabricatorApplicationTransaction',
     'PhabricatorProjectTransactionEditor' => 'PhabricatorApplicationTransactionEditor',
     'PhabricatorProjectTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
     'PhabricatorProjectUIEventListener' => 'PhabricatorEventListener',
     'PhabricatorProjectUpdateController' => 'PhabricatorProjectController',
     'PhabricatorProjectViewController' => 'PhabricatorProjectController',
     'PhabricatorProjectWatchController' => 'PhabricatorProjectController',
     'PhabricatorProjectsPolicyRule' => 'PhabricatorPolicyRule',
+    'PhabricatorProtocolAdapter' => 'Phobject',
     'PhabricatorPygmentSetupCheck' => 'PhabricatorSetupCheck',
+    'PhabricatorQuery' => 'Phobject',
     'PhabricatorQueryConstraint' => 'Phobject',
     'PhabricatorQueryOrderItem' => 'Phobject',
     'PhabricatorQueryOrderTestCase' => 'PhabricatorTestCase',
     'PhabricatorQueryOrderVector' => array(
       'Phobject',
       'Iterator',
     ),
     'PhabricatorRecaptchaConfigOptions' => 'PhabricatorApplicationConfigOptions',
     'PhabricatorRedirectController' => 'PhabricatorController',
     'PhabricatorRefreshCSRFController' => 'PhabricatorAuthController',
     'PhabricatorRegistrationProfile' => 'Phobject',
     'PhabricatorReleephApplication' => 'PhabricatorApplication',
     'PhabricatorReleephApplicationConfigOptions' => 'PhabricatorApplicationConfigOptions',
     'PhabricatorRemarkupControl' => 'AphrontFormTextAreaControl',
     'PhabricatorRemarkupCowsayBlockInterpreter' => 'PhutilRemarkupBlockInterpreter',
     'PhabricatorRemarkupCustomBlockRule' => 'PhutilRemarkupBlockRule',
     'PhabricatorRemarkupCustomInlineRule' => 'PhutilRemarkupRule',
     'PhabricatorRemarkupFigletBlockInterpreter' => 'PhutilRemarkupBlockInterpreter',
     'PhabricatorRemarkupGraphvizBlockInterpreter' => 'PhutilRemarkupBlockInterpreter',
     'PhabricatorRemarkupUIExample' => 'PhabricatorUIExample',
     'PhabricatorRepositoriesSetupCheck' => 'PhabricatorSetupCheck',
     'PhabricatorRepository' => array(
       'PhabricatorRepositoryDAO',
       'PhabricatorApplicationTransactionInterface',
       'PhabricatorPolicyInterface',
       'PhabricatorFlaggableInterface',
       'PhabricatorMarkupInterface',
       'PhabricatorDestructibleInterface',
       'PhabricatorProjectInterface',
     ),
     'PhabricatorRepositoryArcanistProject' => array(
       'PhabricatorRepositoryDAO',
       'PhabricatorPolicyInterface',
       'PhabricatorDestructibleInterface',
     ),
     'PhabricatorRepositoryArcanistProjectPHIDType' => 'PhabricatorPHIDType',
     'PhabricatorRepositoryArcanistProjectQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'PhabricatorRepositoryAuditRequest' => array(
       'PhabricatorRepositoryDAO',
       'PhabricatorPolicyInterface',
     ),
     'PhabricatorRepositoryBranch' => 'PhabricatorRepositoryDAO',
     'PhabricatorRepositoryCommit' => array(
       'PhabricatorRepositoryDAO',
       'PhabricatorPolicyInterface',
       'PhabricatorFlaggableInterface',
       'PhabricatorProjectInterface',
       'PhabricatorTokenReceiverInterface',
       'PhabricatorSubscribableInterface',
       'PhabricatorMentionableInterface',
       'HarbormasterBuildableInterface',
       'PhabricatorCustomFieldInterface',
       'PhabricatorApplicationTransactionInterface',
     ),
     'PhabricatorRepositoryCommitChangeParserWorker' => 'PhabricatorRepositoryCommitParserWorker',
     'PhabricatorRepositoryCommitData' => 'PhabricatorRepositoryDAO',
     'PhabricatorRepositoryCommitHeraldWorker' => 'PhabricatorRepositoryCommitParserWorker',
     'PhabricatorRepositoryCommitMessageParserWorker' => 'PhabricatorRepositoryCommitParserWorker',
     'PhabricatorRepositoryCommitOwnersWorker' => 'PhabricatorRepositoryCommitParserWorker',
     'PhabricatorRepositoryCommitPHIDType' => 'PhabricatorPHIDType',
     'PhabricatorRepositoryCommitParserWorker' => 'PhabricatorWorker',
+    'PhabricatorRepositoryCommitRef' => 'Phobject',
     'PhabricatorRepositoryCommitSearchIndexer' => 'PhabricatorSearchDocumentIndexer',
     'PhabricatorRepositoryConfigOptions' => 'PhabricatorApplicationConfigOptions',
     'PhabricatorRepositoryDAO' => 'PhabricatorLiskDAO',
     'PhabricatorRepositoryDiscoveryEngine' => 'PhabricatorRepositoryEngine',
     'PhabricatorRepositoryEditor' => 'PhabricatorApplicationTransactionEditor',
+    'PhabricatorRepositoryEngine' => 'Phobject',
     'PhabricatorRepositoryGitCommitChangeParserWorker' => 'PhabricatorRepositoryCommitChangeParserWorker',
     'PhabricatorRepositoryGitCommitMessageParserWorker' => 'PhabricatorRepositoryCommitMessageParserWorker',
+    'PhabricatorRepositoryGraphCache' => 'Phobject',
     'PhabricatorRepositoryGraphStream' => 'Phobject',
     'PhabricatorRepositoryManagementCacheWorkflow' => 'PhabricatorRepositoryManagementWorkflow',
     'PhabricatorRepositoryManagementDiscoverWorkflow' => 'PhabricatorRepositoryManagementWorkflow',
     'PhabricatorRepositoryManagementEditWorkflow' => 'PhabricatorRepositoryManagementWorkflow',
     'PhabricatorRepositoryManagementImportingWorkflow' => 'PhabricatorRepositoryManagementWorkflow',
     'PhabricatorRepositoryManagementListWorkflow' => 'PhabricatorRepositoryManagementWorkflow',
     'PhabricatorRepositoryManagementLookupUsersWorkflow' => 'PhabricatorRepositoryManagementWorkflow',
     'PhabricatorRepositoryManagementMarkImportedWorkflow' => 'PhabricatorRepositoryManagementWorkflow',
     'PhabricatorRepositoryManagementMirrorWorkflow' => 'PhabricatorRepositoryManagementWorkflow',
     'PhabricatorRepositoryManagementParentsWorkflow' => 'PhabricatorRepositoryManagementWorkflow',
     'PhabricatorRepositoryManagementPullWorkflow' => 'PhabricatorRepositoryManagementWorkflow',
     'PhabricatorRepositoryManagementRefsWorkflow' => 'PhabricatorRepositoryManagementWorkflow',
     'PhabricatorRepositoryManagementReparseWorkflow' => 'PhabricatorRepositoryManagementWorkflow',
     'PhabricatorRepositoryManagementUpdateWorkflow' => 'PhabricatorRepositoryManagementWorkflow',
     'PhabricatorRepositoryManagementWorkflow' => 'PhabricatorManagementWorkflow',
     'PhabricatorRepositoryMercurialCommitChangeParserWorker' => 'PhabricatorRepositoryCommitChangeParserWorker',
     'PhabricatorRepositoryMercurialCommitMessageParserWorker' => 'PhabricatorRepositoryCommitMessageParserWorker',
     'PhabricatorRepositoryMirror' => array(
       'PhabricatorRepositoryDAO',
       'PhabricatorPolicyInterface',
     ),
     'PhabricatorRepositoryMirrorEngine' => 'PhabricatorRepositoryEngine',
     'PhabricatorRepositoryMirrorPHIDType' => 'PhabricatorPHIDType',
     'PhabricatorRepositoryMirrorQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'PhabricatorRepositoryParsedChange' => 'Phobject',
     'PhabricatorRepositoryPullEngine' => 'PhabricatorRepositoryEngine',
     'PhabricatorRepositoryPullLocalDaemon' => 'PhabricatorDaemon',
     'PhabricatorRepositoryPushEvent' => array(
       'PhabricatorRepositoryDAO',
       'PhabricatorPolicyInterface',
     ),
     'PhabricatorRepositoryPushEventPHIDType' => 'PhabricatorPHIDType',
     'PhabricatorRepositoryPushEventQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'PhabricatorRepositoryPushLog' => array(
       'PhabricatorRepositoryDAO',
       'PhabricatorPolicyInterface',
     ),
     'PhabricatorRepositoryPushLogPHIDType' => 'PhabricatorPHIDType',
     'PhabricatorRepositoryPushLogQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'PhabricatorRepositoryPushLogSearchEngine' => 'PhabricatorApplicationSearchEngine',
     'PhabricatorRepositoryPushMailWorker' => 'PhabricatorWorker',
     'PhabricatorRepositoryPushReplyHandler' => 'PhabricatorMailReplyHandler',
     'PhabricatorRepositoryQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'PhabricatorRepositoryRefCursor' => array(
       'PhabricatorRepositoryDAO',
       'PhabricatorPolicyInterface',
     ),
     'PhabricatorRepositoryRefCursorQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'PhabricatorRepositoryRefEngine' => 'PhabricatorRepositoryEngine',
     'PhabricatorRepositoryRepositoryPHIDType' => 'PhabricatorPHIDType',
     'PhabricatorRepositorySchemaSpec' => 'PhabricatorConfigSchemaSpec',
     'PhabricatorRepositorySearchEngine' => 'PhabricatorApplicationSearchEngine',
     'PhabricatorRepositoryStatusMessage' => 'PhabricatorRepositoryDAO',
     'PhabricatorRepositorySvnCommitChangeParserWorker' => 'PhabricatorRepositoryCommitChangeParserWorker',
     'PhabricatorRepositorySvnCommitMessageParserWorker' => 'PhabricatorRepositoryCommitMessageParserWorker',
     'PhabricatorRepositorySymbol' => 'PhabricatorRepositoryDAO',
     'PhabricatorRepositoryTestCase' => 'PhabricatorTestCase',
     'PhabricatorRepositoryTransaction' => 'PhabricatorApplicationTransaction',
     'PhabricatorRepositoryTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
+    'PhabricatorRepositoryType' => 'Phobject',
     'PhabricatorRepositoryURINormalizer' => 'Phobject',
     'PhabricatorRepositoryURINormalizerTestCase' => 'PhabricatorTestCase',
     'PhabricatorRepositoryURITestCase' => 'PhabricatorTestCase',
     'PhabricatorRepositoryVCSPassword' => 'PhabricatorRepositoryDAO',
     'PhabricatorRepositoryVersion' => 'Phobject',
     'PhabricatorRobotsController' => 'PhabricatorController',
     'PhabricatorS3FileStorageEngine' => 'PhabricatorFileStorageEngine',
     'PhabricatorSMS' => 'PhabricatorSMSDAO',
     'PhabricatorSMSConfigOptions' => 'PhabricatorApplicationConfigOptions',
     'PhabricatorSMSDAO' => 'PhabricatorLiskDAO',
     'PhabricatorSMSDemultiplexWorker' => 'PhabricatorSMSWorker',
+    'PhabricatorSMSImplementationAdapter' => 'Phobject',
     'PhabricatorSMSImplementationTestBlackholeAdapter' => 'PhabricatorSMSImplementationAdapter',
     'PhabricatorSMSImplementationTwilioAdapter' => 'PhabricatorSMSImplementationAdapter',
     'PhabricatorSMSManagementListOutboundWorkflow' => 'PhabricatorSMSManagementWorkflow',
     'PhabricatorSMSManagementSendTestWorkflow' => 'PhabricatorSMSManagementWorkflow',
     'PhabricatorSMSManagementShowOutboundWorkflow' => 'PhabricatorSMSManagementWorkflow',
     'PhabricatorSMSManagementWorkflow' => 'PhabricatorManagementWorkflow',
     'PhabricatorSMSSendWorker' => 'PhabricatorSMSWorker',
     'PhabricatorSMSWorker' => 'PhabricatorWorker',
+    'PhabricatorSQLPatchList' => 'Phobject',
     'PhabricatorSSHKeyGenerator' => 'Phobject',
     'PhabricatorSSHKeysSettingsPanel' => 'PhabricatorSettingsPanel',
     'PhabricatorSSHLog' => 'Phobject',
     'PhabricatorSSHPassthruCommand' => 'Phobject',
     'PhabricatorSSHWorkflow' => 'PhabricatorManagementWorkflow',
     'PhabricatorSavedQuery' => array(
       'PhabricatorSearchDAO',
       'PhabricatorPolicyInterface',
     ),
     'PhabricatorSavedQueryQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'PhabricatorScheduleTaskTriggerAction' => 'PhabricatorTriggerAction',
+    'PhabricatorScopedEnv' => 'Phobject',
+    'PhabricatorSearchAbstractDocument' => 'Phobject',
     'PhabricatorSearchApplication' => 'PhabricatorApplication',
     'PhabricatorSearchApplicationSearchEngine' => 'PhabricatorApplicationSearchEngine',
     'PhabricatorSearchApplicationStorageEnginePanel' => 'PhabricatorApplicationConfigurationPanel',
     'PhabricatorSearchAttachController' => 'PhabricatorSearchBaseController',
     'PhabricatorSearchBaseController' => 'PhabricatorController',
     'PhabricatorSearchCheckboxesField' => 'PhabricatorSearchField',
     'PhabricatorSearchConfigOptions' => 'PhabricatorApplicationConfigOptions',
     'PhabricatorSearchController' => 'PhabricatorSearchBaseController',
     'PhabricatorSearchCustomFieldProxyField' => 'PhabricatorSearchField',
     'PhabricatorSearchDAO' => 'PhabricatorLiskDAO',
     'PhabricatorSearchDatasource' => 'PhabricatorTypeaheadCompositeDatasource',
     'PhabricatorSearchDatasourceField' => 'PhabricatorSearchTokenizerField',
     'PhabricatorSearchDateField' => 'PhabricatorSearchField',
     'PhabricatorSearchDeleteController' => 'PhabricatorSearchBaseController',
     'PhabricatorSearchDocument' => 'PhabricatorSearchDAO',
     'PhabricatorSearchDocumentField' => 'PhabricatorSearchDAO',
     'PhabricatorSearchDocumentFieldType' => 'Phobject',
     'PhabricatorSearchDocumentIndexer' => 'Phobject',
     'PhabricatorSearchDocumentQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'PhabricatorSearchDocumentRelationship' => 'PhabricatorSearchDAO',
     'PhabricatorSearchDocumentTypeDatasource' => 'PhabricatorTypeaheadDatasource',
     'PhabricatorSearchEditController' => 'PhabricatorSearchBaseController',
+    'PhabricatorSearchEngine' => 'Phobject',
+    'PhabricatorSearchEngineTestCase' => 'PhabricatorTestCase',
     'PhabricatorSearchField' => 'Phobject',
     'PhabricatorSearchHovercardController' => 'PhabricatorSearchBaseController',
+    'PhabricatorSearchIndexer' => 'Phobject',
     'PhabricatorSearchManagementIndexWorkflow' => 'PhabricatorSearchManagementWorkflow',
     'PhabricatorSearchManagementInitWorkflow' => 'PhabricatorSearchManagementWorkflow',
     'PhabricatorSearchManagementWorkflow' => 'PhabricatorManagementWorkflow',
     'PhabricatorSearchOrderController' => 'PhabricatorSearchBaseController',
     'PhabricatorSearchOrderField' => 'PhabricatorSearchField',
     'PhabricatorSearchOwnersField' => 'PhabricatorSearchTokenizerField',
     'PhabricatorSearchPreferencesSettingsPanel' => 'PhabricatorSettingsPanel',
     'PhabricatorSearchProjectsField' => 'PhabricatorSearchTokenizerField',
+    'PhabricatorSearchRelationship' => 'Phobject',
     'PhabricatorSearchResultView' => 'AphrontView',
     'PhabricatorSearchSelectController' => 'PhabricatorSearchBaseController',
     'PhabricatorSearchSelectField' => 'PhabricatorSearchField',
     'PhabricatorSearchSpacesField' => 'PhabricatorSearchTokenizerField',
     'PhabricatorSearchStringListField' => 'PhabricatorSearchField',
     'PhabricatorSearchSubscribersField' => 'PhabricatorSearchTokenizerField',
     'PhabricatorSearchTextField' => 'PhabricatorSearchField',
     'PhabricatorSearchThreeStateField' => 'PhabricatorSearchField',
     'PhabricatorSearchTokenizerField' => 'PhabricatorSearchField',
     'PhabricatorSearchUsersField' => 'PhabricatorSearchTokenizerField',
     'PhabricatorSearchWorker' => 'PhabricatorWorker',
     'PhabricatorSecurityConfigOptions' => 'PhabricatorApplicationConfigOptions',
     'PhabricatorSecuritySetupCheck' => 'PhabricatorSetupCheck',
     'PhabricatorSendGridConfigOptions' => 'PhabricatorApplicationConfigOptions',
     'PhabricatorSessionsSettingsPanel' => 'PhabricatorSettingsPanel',
     'PhabricatorSettingsAddEmailAction' => 'PhabricatorSystemAction',
     'PhabricatorSettingsAdjustController' => 'PhabricatorController',
     'PhabricatorSettingsApplication' => 'PhabricatorApplication',
     'PhabricatorSettingsMainController' => 'PhabricatorController',
+    'PhabricatorSettingsPanel' => 'Phobject',
+    'PhabricatorSetupCheck' => 'Phobject',
+    'PhabricatorSetupCheckTestCase' => 'PhabricatorTestCase',
+    'PhabricatorSetupIssue' => 'Phobject',
     'PhabricatorSetupIssueUIExample' => 'PhabricatorUIExample',
     'PhabricatorSetupIssueView' => 'AphrontView',
     'PhabricatorSlowvoteApplication' => 'PhabricatorApplication',
     'PhabricatorSlowvoteChoice' => 'PhabricatorSlowvoteDAO',
     'PhabricatorSlowvoteCloseController' => 'PhabricatorSlowvoteController',
     'PhabricatorSlowvoteCommentController' => 'PhabricatorSlowvoteController',
     'PhabricatorSlowvoteController' => 'PhabricatorController',
     'PhabricatorSlowvoteDAO' => 'PhabricatorLiskDAO',
     'PhabricatorSlowvoteDefaultViewCapability' => 'PhabricatorPolicyCapability',
     'PhabricatorSlowvoteEditController' => 'PhabricatorSlowvoteController',
     'PhabricatorSlowvoteEditor' => 'PhabricatorApplicationTransactionEditor',
     'PhabricatorSlowvoteListController' => 'PhabricatorSlowvoteController',
     'PhabricatorSlowvoteOption' => 'PhabricatorSlowvoteDAO',
     'PhabricatorSlowvotePoll' => array(
       'PhabricatorSlowvoteDAO',
       'PhabricatorApplicationTransactionInterface',
       'PhabricatorPolicyInterface',
       'PhabricatorSubscribableInterface',
       'PhabricatorFlaggableInterface',
       'PhabricatorTokenReceiverInterface',
       'PhabricatorProjectInterface',
       'PhabricatorDestructibleInterface',
     ),
     'PhabricatorSlowvotePollController' => 'PhabricatorSlowvoteController',
     'PhabricatorSlowvotePollPHIDType' => 'PhabricatorPHIDType',
     'PhabricatorSlowvoteQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'PhabricatorSlowvoteSchemaSpec' => 'PhabricatorConfigSchemaSpec',
     'PhabricatorSlowvoteSearchEngine' => 'PhabricatorApplicationSearchEngine',
     'PhabricatorSlowvoteTransaction' => 'PhabricatorApplicationTransaction',
     'PhabricatorSlowvoteTransactionComment' => 'PhabricatorApplicationTransactionComment',
     'PhabricatorSlowvoteTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
     'PhabricatorSlowvoteVoteController' => 'PhabricatorSlowvoteController',
+    'PhabricatorSlug' => 'Phobject',
     'PhabricatorSlugTestCase' => 'PhabricatorTestCase',
     'PhabricatorSortTableUIExample' => 'PhabricatorUIExample',
     'PhabricatorSourceCodeView' => 'AphrontView',
     'PhabricatorSpacesApplication' => 'PhabricatorApplication',
+    'PhabricatorSpacesArchiveController' => 'PhabricatorSpacesController',
     'PhabricatorSpacesCapabilityCreateSpaces' => 'PhabricatorPolicyCapability',
     'PhabricatorSpacesCapabilityDefaultEdit' => 'PhabricatorPolicyCapability',
     'PhabricatorSpacesCapabilityDefaultView' => 'PhabricatorPolicyCapability',
-    'PhabricatorSpacesControl' => 'AphrontFormControl',
     'PhabricatorSpacesController' => 'PhabricatorController',
     'PhabricatorSpacesDAO' => 'PhabricatorLiskDAO',
     'PhabricatorSpacesEditController' => 'PhabricatorSpacesController',
     'PhabricatorSpacesInterface' => 'PhabricatorPHIDInterface',
     'PhabricatorSpacesListController' => 'PhabricatorSpacesController',
     'PhabricatorSpacesNamespace' => array(
       'PhabricatorSpacesDAO',
       'PhabricatorPolicyInterface',
       'PhabricatorApplicationTransactionInterface',
       'PhabricatorDestructibleInterface',
     ),
     'PhabricatorSpacesNamespaceDatasource' => 'PhabricatorTypeaheadDatasource',
     'PhabricatorSpacesNamespaceEditor' => 'PhabricatorApplicationTransactionEditor',
     'PhabricatorSpacesNamespacePHIDType' => 'PhabricatorPHIDType',
     'PhabricatorSpacesNamespaceQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'PhabricatorSpacesNamespaceSearchEngine' => 'PhabricatorApplicationSearchEngine',
     'PhabricatorSpacesNamespaceTransaction' => 'PhabricatorApplicationTransaction',
     'PhabricatorSpacesNamespaceTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
     'PhabricatorSpacesRemarkupRule' => 'PhabricatorObjectRemarkupRule',
+    'PhabricatorSpacesSchemaSpec' => 'PhabricatorConfigSchemaSpec',
     'PhabricatorSpacesTestCase' => 'PhabricatorTestCase',
     'PhabricatorSpacesViewController' => 'PhabricatorSpacesController',
     'PhabricatorStandardCustomField' => 'PhabricatorCustomField',
     'PhabricatorStandardCustomFieldBool' => 'PhabricatorStandardCustomField',
     'PhabricatorStandardCustomFieldCredential' => 'PhabricatorStandardCustomField',
     'PhabricatorStandardCustomFieldDate' => 'PhabricatorStandardCustomField',
     'PhabricatorStandardCustomFieldHeader' => 'PhabricatorStandardCustomField',
     'PhabricatorStandardCustomFieldInt' => 'PhabricatorStandardCustomField',
     'PhabricatorStandardCustomFieldLink' => 'PhabricatorStandardCustomField',
     'PhabricatorStandardCustomFieldPHIDs' => 'PhabricatorStandardCustomField',
     'PhabricatorStandardCustomFieldRemarkup' => 'PhabricatorStandardCustomField',
     'PhabricatorStandardCustomFieldSelect' => 'PhabricatorStandardCustomField',
     'PhabricatorStandardCustomFieldText' => 'PhabricatorStandardCustomField',
     'PhabricatorStandardCustomFieldUsers' => 'PhabricatorStandardCustomFieldPHIDs',
     'PhabricatorStandardPageView' => 'PhabricatorBarePageView',
     'PhabricatorStatusController' => 'PhabricatorController',
     'PhabricatorStatusUIExample' => 'PhabricatorUIExample',
+    'PhabricatorStorageFixtureScopeGuard' => 'Phobject',
+    'PhabricatorStorageManagementAPI' => 'Phobject',
     'PhabricatorStorageManagementAdjustWorkflow' => 'PhabricatorStorageManagementWorkflow',
     'PhabricatorStorageManagementDatabasesWorkflow' => 'PhabricatorStorageManagementWorkflow',
     'PhabricatorStorageManagementDestroyWorkflow' => 'PhabricatorStorageManagementWorkflow',
     'PhabricatorStorageManagementDumpWorkflow' => 'PhabricatorStorageManagementWorkflow',
     'PhabricatorStorageManagementProbeWorkflow' => 'PhabricatorStorageManagementWorkflow',
     'PhabricatorStorageManagementQuickstartWorkflow' => 'PhabricatorStorageManagementWorkflow',
     'PhabricatorStorageManagementRenamespaceWorkflow' => 'PhabricatorStorageManagementWorkflow',
     'PhabricatorStorageManagementShellWorkflow' => 'PhabricatorStorageManagementWorkflow',
     'PhabricatorStorageManagementStatusWorkflow' => 'PhabricatorStorageManagementWorkflow',
     'PhabricatorStorageManagementUpgradeWorkflow' => 'PhabricatorStorageManagementWorkflow',
     'PhabricatorStorageManagementWorkflow' => 'PhabricatorManagementWorkflow',
+    'PhabricatorStoragePatch' => 'Phobject',
     'PhabricatorStorageSchemaSpec' => 'PhabricatorConfigSchemaSpec',
     'PhabricatorStorageSetupCheck' => 'PhabricatorSetupCheck',
     'PhabricatorStreamingProtocolAdapter' => 'PhabricatorProtocolAdapter',
     'PhabricatorSubscribedToObjectEdgeType' => 'PhabricatorEdgeType',
     'PhabricatorSubscribersQuery' => 'PhabricatorQuery',
     'PhabricatorSubscriptionTriggerClock' => 'PhabricatorTriggerClock',
     'PhabricatorSubscriptionsApplication' => 'PhabricatorApplication',
     'PhabricatorSubscriptionsEditController' => 'PhabricatorController',
     'PhabricatorSubscriptionsEditor' => 'PhabricatorEditor',
     'PhabricatorSubscriptionsListController' => 'PhabricatorController',
     'PhabricatorSubscriptionsSubscribeEmailCommand' => 'MetaMTAEmailTransactionCommand',
+    'PhabricatorSubscriptionsSubscribersPolicyRule' => 'PhabricatorPolicyRule',
     'PhabricatorSubscriptionsTransactionController' => 'PhabricatorController',
     'PhabricatorSubscriptionsUIEventListener' => 'PhabricatorEventListener',
     'PhabricatorSubscriptionsUnsubscribeEmailCommand' => 'MetaMTAEmailTransactionCommand',
     'PhabricatorSupportApplication' => 'PhabricatorApplication',
+    'PhabricatorSyntaxHighlighter' => 'Phobject',
     'PhabricatorSyntaxHighlightingConfigOptions' => 'PhabricatorApplicationConfigOptions',
+    'PhabricatorSystemAction' => 'Phobject',
     'PhabricatorSystemActionEngine' => 'Phobject',
     'PhabricatorSystemActionGarbageCollector' => 'PhabricatorGarbageCollector',
     'PhabricatorSystemActionLog' => 'PhabricatorSystemDAO',
     'PhabricatorSystemActionRateLimitException' => 'Exception',
     'PhabricatorSystemApplication' => 'PhabricatorApplication',
     'PhabricatorSystemDAO' => 'PhabricatorLiskDAO',
     'PhabricatorSystemDestructionGarbageCollector' => 'PhabricatorGarbageCollector',
     'PhabricatorSystemDestructionLog' => 'PhabricatorSystemDAO',
     'PhabricatorSystemRemoveDestroyWorkflow' => 'PhabricatorSystemRemoveWorkflow',
     'PhabricatorSystemRemoveLogWorkflow' => 'PhabricatorSystemRemoveWorkflow',
     'PhabricatorSystemRemoveWorkflow' => 'PhabricatorManagementWorkflow',
     'PhabricatorSystemSelectEncodingController' => 'PhabricatorController',
     'PhabricatorSystemSelectHighlightController' => 'PhabricatorController',
     'PhabricatorTOTPAuthFactor' => 'PhabricatorAuthFactor',
     'PhabricatorTOTPAuthFactorTestCase' => 'PhabricatorTestCase',
     'PhabricatorTaskmasterDaemon' => 'PhabricatorDaemon',
     'PhabricatorTestApplication' => 'PhabricatorApplication',
     'PhabricatorTestCase' => 'PhutilTestCase',
     'PhabricatorTestController' => 'PhabricatorController',
+    'PhabricatorTestDataGenerator' => 'Phobject',
     'PhabricatorTestNoCycleEdgeType' => 'PhabricatorEdgeType',
     'PhabricatorTestStorageEngine' => 'PhabricatorFileStorageEngine',
     'PhabricatorTestWorker' => 'PhabricatorWorker',
+    'PhabricatorTime' => 'Phobject',
+    'PhabricatorTimeGuard' => 'Phobject',
     'PhabricatorTimeTestCase' => 'PhabricatorTestCase',
     'PhabricatorTimezoneSetupCheck' => 'PhabricatorSetupCheck',
     'PhabricatorToken' => array(
       'PhabricatorTokenDAO',
       'PhabricatorPolicyInterface',
     ),
     'PhabricatorTokenController' => 'PhabricatorController',
     'PhabricatorTokenCount' => 'PhabricatorTokenDAO',
     'PhabricatorTokenCountQuery' => 'PhabricatorOffsetPagedQuery',
     'PhabricatorTokenDAO' => 'PhabricatorLiskDAO',
     'PhabricatorTokenGiveController' => 'PhabricatorTokenController',
     'PhabricatorTokenGiven' => array(
       'PhabricatorTokenDAO',
       'PhabricatorPolicyInterface',
     ),
     'PhabricatorTokenGivenController' => 'PhabricatorTokenController',
     'PhabricatorTokenGivenEditor' => 'PhabricatorEditor',
     'PhabricatorTokenGivenFeedStory' => 'PhabricatorFeedStory',
     'PhabricatorTokenGivenQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'PhabricatorTokenLeaderController' => 'PhabricatorTokenController',
     'PhabricatorTokenQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'PhabricatorTokenReceiverQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'PhabricatorTokenTokenPHIDType' => 'PhabricatorPHIDType',
     'PhabricatorTokenUIEventListener' => 'PhabricatorEventListener',
     'PhabricatorTokensApplication' => 'PhabricatorApplication',
     'PhabricatorTokensSettingsPanel' => 'PhabricatorSettingsPanel',
     'PhabricatorTooltipUIExample' => 'PhabricatorUIExample',
+    'PhabricatorTransactions' => 'Phobject',
     'PhabricatorTransactionsApplication' => 'PhabricatorApplication',
     'PhabricatorTransformedFile' => 'PhabricatorFileDAO',
     'PhabricatorTranslationsConfigOptions' => 'PhabricatorApplicationConfigOptions',
     'PhabricatorTriggerAction' => 'Phobject',
     'PhabricatorTriggerClock' => 'Phobject',
     'PhabricatorTriggerClockTestCase' => 'PhabricatorTestCase',
     'PhabricatorTriggerDaemon' => 'PhabricatorDaemon',
     'PhabricatorTrivialTestCase' => 'PhabricatorTestCase',
     'PhabricatorTwitchAuthProvider' => 'PhabricatorOAuth2AuthProvider',
     'PhabricatorTwitterAuthProvider' => 'PhabricatorOAuth1AuthProvider',
     'PhabricatorTwoColumnUIExample' => 'PhabricatorUIExample',
     'PhabricatorTypeaheadApplication' => 'PhabricatorApplication',
     'PhabricatorTypeaheadCompositeDatasource' => 'PhabricatorTypeaheadDatasource',
     'PhabricatorTypeaheadDatasource' => 'Phobject',
     'PhabricatorTypeaheadDatasourceController' => 'PhabricatorController',
     'PhabricatorTypeaheadFunctionHelpController' => 'PhabricatorTypeaheadDatasourceController',
     'PhabricatorTypeaheadInvalidTokenException' => 'Exception',
     'PhabricatorTypeaheadModularDatasourceController' => 'PhabricatorTypeaheadDatasourceController',
     'PhabricatorTypeaheadMonogramDatasource' => 'PhabricatorTypeaheadDatasource',
+    'PhabricatorTypeaheadResult' => 'Phobject',
     'PhabricatorTypeaheadRuntimeCompositeDatasource' => 'PhabricatorTypeaheadCompositeDatasource',
     'PhabricatorTypeaheadTokenView' => 'AphrontTagView',
     'PhabricatorUIConfigOptions' => 'PhabricatorApplicationConfigOptions',
+    'PhabricatorUIExample' => 'Phobject',
     'PhabricatorUIExampleRenderController' => 'PhabricatorController',
     'PhabricatorUIExamplesApplication' => 'PhabricatorApplication',
     'PhabricatorUSEnglishTranslation' => 'PhutilTranslation',
     'PhabricatorUnitsTestCase' => 'PhabricatorTestCase',
     'PhabricatorUnsubscribedFromObjectEdgeType' => 'PhabricatorEdgeType',
     'PhabricatorUser' => array(
       'PhabricatorUserDAO',
       'PhutilPerson',
       'PhabricatorPolicyInterface',
       'PhabricatorCustomFieldInterface',
       'PhabricatorDestructibleInterface',
       'PhabricatorSSHPublicKeyInterface',
       'PhabricatorApplicationTransactionInterface',
     ),
     'PhabricatorUserBlurbField' => 'PhabricatorUserCustomField',
     'PhabricatorUserConfigOptions' => 'PhabricatorApplicationConfigOptions',
     'PhabricatorUserConfiguredCustomField' => array(
       'PhabricatorUserCustomField',
       'PhabricatorStandardCustomFieldInterface',
     ),
     'PhabricatorUserConfiguredCustomFieldStorage' => 'PhabricatorCustomFieldStorage',
     'PhabricatorUserCustomField' => 'PhabricatorCustomField',
     'PhabricatorUserCustomFieldNumericIndex' => 'PhabricatorCustomFieldNumericIndexStorage',
     'PhabricatorUserCustomFieldStringIndex' => 'PhabricatorCustomFieldStringIndexStorage',
     'PhabricatorUserDAO' => 'PhabricatorLiskDAO',
     'PhabricatorUserEditor' => 'PhabricatorEditor',
     'PhabricatorUserEditorTestCase' => 'PhabricatorTestCase',
     'PhabricatorUserEmail' => 'PhabricatorUserDAO',
     'PhabricatorUserEmailTestCase' => 'PhabricatorTestCase',
     'PhabricatorUserLog' => array(
       'PhabricatorUserDAO',
       'PhabricatorPolicyInterface',
     ),
     'PhabricatorUserLogView' => 'AphrontView',
     'PhabricatorUserPreferences' => 'PhabricatorUserDAO',
     'PhabricatorUserProfile' => 'PhabricatorUserDAO',
     'PhabricatorUserProfileEditor' => 'PhabricatorApplicationTransactionEditor',
     'PhabricatorUserRealNameField' => 'PhabricatorUserCustomField',
     'PhabricatorUserRolesField' => 'PhabricatorUserCustomField',
     'PhabricatorUserSchemaSpec' => 'PhabricatorConfigSchemaSpec',
     'PhabricatorUserSearchIndexer' => 'PhabricatorSearchDocumentIndexer',
     'PhabricatorUserSinceField' => 'PhabricatorUserCustomField',
     'PhabricatorUserStatusField' => 'PhabricatorUserCustomField',
     'PhabricatorUserTestCase' => 'PhabricatorTestCase',
     'PhabricatorUserTitleField' => 'PhabricatorUserCustomField',
     'PhabricatorUserTransaction' => 'PhabricatorApplicationTransaction',
     'PhabricatorUsersPolicyRule' => 'PhabricatorPolicyRule',
     'PhabricatorVCSResponse' => 'AphrontResponse',
     'PhabricatorVeryWowEnglishTranslation' => 'PhutilTranslation',
     'PhabricatorViewerDatasource' => 'PhabricatorTypeaheadDatasource',
     'PhabricatorWatcherHasObjectEdgeType' => 'PhabricatorEdgeType',
     'PhabricatorWordPressAuthProvider' => 'PhabricatorOAuth2AuthProvider',
+    'PhabricatorWorker' => 'Phobject',
     'PhabricatorWorkerActiveTask' => 'PhabricatorWorkerTask',
     'PhabricatorWorkerArchiveTask' => 'PhabricatorWorkerTask',
     'PhabricatorWorkerArchiveTaskQuery' => 'PhabricatorQuery',
     'PhabricatorWorkerDAO' => 'PhabricatorLiskDAO',
     'PhabricatorWorkerLeaseQuery' => 'PhabricatorQuery',
     'PhabricatorWorkerManagementCancelWorkflow' => 'PhabricatorWorkerManagementWorkflow',
     'PhabricatorWorkerManagementExecuteWorkflow' => 'PhabricatorWorkerManagementWorkflow',
     'PhabricatorWorkerManagementFloodWorkflow' => 'PhabricatorWorkerManagementWorkflow',
     'PhabricatorWorkerManagementFreeWorkflow' => 'PhabricatorWorkerManagementWorkflow',
     'PhabricatorWorkerManagementRetryWorkflow' => 'PhabricatorWorkerManagementWorkflow',
     'PhabricatorWorkerManagementWorkflow' => 'PhabricatorManagementWorkflow',
     'PhabricatorWorkerPermanentFailureException' => 'Exception',
     'PhabricatorWorkerTask' => 'PhabricatorWorkerDAO',
     'PhabricatorWorkerTaskData' => 'PhabricatorWorkerDAO',
     'PhabricatorWorkerTaskDetailController' => 'PhabricatorDaemonController',
     'PhabricatorWorkerTestCase' => 'PhabricatorTestCase',
     'PhabricatorWorkerTrigger' => array(
       'PhabricatorWorkerDAO',
       'PhabricatorDestructibleInterface',
       'PhabricatorPolicyInterface',
     ),
     'PhabricatorWorkerTriggerEvent' => 'PhabricatorWorkerDAO',
     'PhabricatorWorkerTriggerManagementFireWorkflow' => 'PhabricatorWorkerTriggerManagementWorkflow',
     'PhabricatorWorkerTriggerManagementWorkflow' => 'PhabricatorManagementWorkflow',
     'PhabricatorWorkerTriggerPHIDType' => 'PhabricatorPHIDType',
     'PhabricatorWorkerTriggerQuery' => 'PhabricatorPolicyAwareQuery',
     'PhabricatorWorkerYieldException' => 'Exception',
     'PhabricatorWorkingCopyDiscoveryTestCase' => 'PhabricatorWorkingCopyTestCase',
     'PhabricatorWorkingCopyPullTestCase' => 'PhabricatorWorkingCopyTestCase',
     'PhabricatorWorkingCopyTestCase' => 'PhabricatorTestCase',
     'PhabricatorXHPASTViewController' => 'PhabricatorController',
     'PhabricatorXHPASTViewDAO' => 'PhabricatorLiskDAO',
     'PhabricatorXHPASTViewFrameController' => 'PhabricatorXHPASTViewController',
     'PhabricatorXHPASTViewFramesetController' => 'PhabricatorXHPASTViewController',
     'PhabricatorXHPASTViewInputController' => 'PhabricatorXHPASTViewPanelController',
     'PhabricatorXHPASTViewPanelController' => 'PhabricatorXHPASTViewController',
     'PhabricatorXHPASTViewParseTree' => 'PhabricatorXHPASTViewDAO',
     'PhabricatorXHPASTViewRunController' => 'PhabricatorXHPASTViewController',
     'PhabricatorXHPASTViewStreamController' => 'PhabricatorXHPASTViewPanelController',
     'PhabricatorXHPASTViewTreeController' => 'PhabricatorXHPASTViewPanelController',
     'PhabricatorXHProfApplication' => 'PhabricatorApplication',
     'PhabricatorXHProfController' => 'PhabricatorController',
     'PhabricatorXHProfDAO' => 'PhabricatorLiskDAO',
     'PhabricatorXHProfProfileController' => 'PhabricatorXHProfController',
     'PhabricatorXHProfProfileSymbolView' => 'PhabricatorXHProfProfileView',
     'PhabricatorXHProfProfileTopLevelView' => 'PhabricatorXHProfProfileView',
     'PhabricatorXHProfProfileView' => 'AphrontView',
     'PhabricatorXHProfSample' => 'PhabricatorXHProfDAO',
     'PhabricatorXHProfSampleListController' => 'PhabricatorXHProfController',
     'PhabricatorYoutubeRemarkupRule' => 'PhutilRemarkupRule',
     'PhameBasicBlogSkin' => 'PhameBlogSkin',
     'PhameBasicTemplateBlogSkin' => 'PhameBasicBlogSkin',
     'PhameBlog' => array(
       'PhameDAO',
       'PhabricatorPolicyInterface',
       'PhabricatorMarkupInterface',
       'PhabricatorApplicationTransactionInterface',
     ),
     'PhameBlogDeleteController' => 'PhameController',
     'PhameBlogEditController' => 'PhameController',
     'PhameBlogEditor' => 'PhabricatorApplicationTransactionEditor',
     'PhameBlogFeedController' => 'PhameController',
     'PhameBlogListController' => 'PhameController',
     'PhameBlogLiveController' => 'PhameController',
     'PhameBlogQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'PhameBlogSkin' => 'PhabricatorController',
     'PhameBlogTransaction' => 'PhabricatorApplicationTransaction',
     'PhameBlogViewController' => 'PhameController',
     'PhameCelerityResources' => 'CelerityResources',
     'PhameConduitAPIMethod' => 'ConduitAPIMethod',
     'PhameController' => 'PhabricatorController',
     'PhameCreatePostConduitAPIMethod' => 'PhameConduitAPIMethod',
     'PhameDAO' => 'PhabricatorLiskDAO',
     'PhamePost' => array(
       'PhameDAO',
       'PhabricatorPolicyInterface',
       'PhabricatorMarkupInterface',
       'PhabricatorApplicationTransactionInterface',
       'PhabricatorTokenReceiverInterface',
     ),
     'PhamePostDeleteController' => 'PhameController',
     'PhamePostEditController' => 'PhameController',
     'PhamePostEditor' => 'PhabricatorApplicationTransactionEditor',
     'PhamePostFramedController' => 'PhameController',
     'PhamePostListController' => 'PhameController',
     'PhamePostNewController' => 'PhameController',
     'PhamePostNotLiveController' => 'PhameController',
     'PhamePostPreviewController' => 'PhameController',
     'PhamePostPublishController' => 'PhameController',
     'PhamePostQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'PhamePostTransaction' => 'PhabricatorApplicationTransaction',
     'PhamePostTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
     'PhamePostUnpublishController' => 'PhameController',
     'PhamePostView' => 'AphrontView',
     'PhamePostViewController' => 'PhameController',
     'PhameQueryConduitAPIMethod' => 'PhameConduitAPIMethod',
     'PhameQueryPostsConduitAPIMethod' => 'PhameConduitAPIMethod',
     'PhameResourceController' => 'CelerityResourceController',
     'PhameSchemaSpec' => 'PhabricatorConfigSchemaSpec',
+    'PhameSkinSpecification' => 'Phobject',
     'PhluxController' => 'PhabricatorController',
     'PhluxDAO' => 'PhabricatorLiskDAO',
     'PhluxEditController' => 'PhluxController',
     'PhluxListController' => 'PhluxController',
     'PhluxTransaction' => 'PhabricatorApplicationTransaction',
     'PhluxTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
     'PhluxVariable' => array(
       'PhluxDAO',
       'PhabricatorApplicationTransactionInterface',
       'PhabricatorFlaggableInterface',
       'PhabricatorPolicyInterface',
     ),
     'PhluxVariableEditor' => 'PhabricatorApplicationTransactionEditor',
     'PhluxVariablePHIDType' => 'PhabricatorPHIDType',
     'PhluxVariableQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'PhluxViewController' => 'PhluxController',
     'PholioActionMenuEventListener' => 'PhabricatorEventListener',
     'PholioController' => 'PhabricatorController',
     'PholioDAO' => 'PhabricatorLiskDAO',
     'PholioDefaultEditCapability' => 'PhabricatorPolicyCapability',
     'PholioDefaultViewCapability' => 'PhabricatorPolicyCapability',
     'PholioImage' => array(
       'PholioDAO',
       'PhabricatorMarkupInterface',
       'PhabricatorPolicyInterface',
     ),
     'PholioImagePHIDType' => 'PhabricatorPHIDType',
     'PholioImageQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'PholioImageUploadController' => 'PholioController',
     'PholioInlineController' => 'PholioController',
     'PholioInlineListController' => 'PholioController',
     'PholioMock' => array(
       'PholioDAO',
       'PhabricatorMarkupInterface',
       'PhabricatorPolicyInterface',
       'PhabricatorSubscribableInterface',
       'PhabricatorTokenReceiverInterface',
       'PhabricatorFlaggableInterface',
       'PhabricatorApplicationTransactionInterface',
       'PhabricatorProjectInterface',
       'PhabricatorDestructibleInterface',
+      'PhabricatorSpacesInterface',
     ),
     'PholioMockCommentController' => 'PholioController',
     'PholioMockEditController' => 'PholioController',
     'PholioMockEditor' => 'PhabricatorApplicationTransactionEditor',
     'PholioMockEmbedView' => 'AphrontView',
     'PholioMockHasTaskEdgeType' => 'PhabricatorEdgeType',
     'PholioMockImagesView' => 'AphrontView',
     'PholioMockListController' => 'PholioController',
     'PholioMockMailReceiver' => 'PhabricatorObjectMailReceiver',
     'PholioMockPHIDType' => 'PhabricatorPHIDType',
     'PholioMockQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'PholioMockSearchEngine' => 'PhabricatorApplicationSearchEngine',
     'PholioMockThumbGridView' => 'AphrontView',
     'PholioMockViewController' => 'PholioController',
     'PholioRemarkupRule' => 'PhabricatorObjectRemarkupRule',
     'PholioReplyHandler' => 'PhabricatorApplicationTransactionReplyHandler',
     'PholioSchemaSpec' => 'PhabricatorConfigSchemaSpec',
     'PholioSearchIndexer' => 'PhabricatorSearchDocumentIndexer',
     'PholioTransaction' => 'PhabricatorApplicationTransaction',
     'PholioTransactionComment' => 'PhabricatorApplicationTransactionComment',
     'PholioTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
     'PholioTransactionView' => 'PhabricatorApplicationTransactionView',
     'PholioUploadedImageView' => 'AphrontView',
     'PhortuneAccount' => array(
       'PhortuneDAO',
       'PhabricatorApplicationTransactionInterface',
       'PhabricatorPolicyInterface',
     ),
     'PhortuneAccountEditController' => 'PhortuneController',
     'PhortuneAccountEditor' => 'PhabricatorApplicationTransactionEditor',
     'PhortuneAccountHasMemberEdgeType' => 'PhabricatorEdgeType',
     'PhortuneAccountListController' => 'PhortuneController',
     'PhortuneAccountPHIDType' => 'PhabricatorPHIDType',
     'PhortuneAccountQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'PhortuneAccountTransaction' => 'PhabricatorApplicationTransaction',
     'PhortuneAccountTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
     'PhortuneAccountViewController' => 'PhortuneController',
     'PhortuneAdHocCart' => 'PhortuneCartImplementation',
     'PhortuneAdHocProduct' => 'PhortuneProductImplementation',
     'PhortuneCart' => array(
       'PhortuneDAO',
       'PhabricatorApplicationTransactionInterface',
       'PhabricatorPolicyInterface',
     ),
     'PhortuneCartAcceptController' => 'PhortuneCartController',
     'PhortuneCartCancelController' => 'PhortuneCartController',
     'PhortuneCartCheckoutController' => 'PhortuneCartController',
     'PhortuneCartController' => 'PhortuneController',
     'PhortuneCartEditor' => 'PhabricatorApplicationTransactionEditor',
+    'PhortuneCartImplementation' => 'Phobject',
     'PhortuneCartListController' => 'PhortuneController',
     'PhortuneCartPHIDType' => 'PhabricatorPHIDType',
     'PhortuneCartQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'PhortuneCartReplyHandler' => 'PhabricatorApplicationTransactionReplyHandler',
     'PhortuneCartSearchEngine' => 'PhabricatorApplicationSearchEngine',
     'PhortuneCartTransaction' => 'PhabricatorApplicationTransaction',
     'PhortuneCartTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
     'PhortuneCartUpdateController' => 'PhortuneCartController',
     'PhortuneCartViewController' => 'PhortuneCartController',
     'PhortuneCharge' => array(
       'PhortuneDAO',
       'PhabricatorPolicyInterface',
     ),
     'PhortuneChargeListController' => 'PhortuneController',
     'PhortuneChargePHIDType' => 'PhabricatorPHIDType',
     'PhortuneChargeQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'PhortuneChargeSearchEngine' => 'PhabricatorApplicationSearchEngine',
     'PhortuneChargeTableView' => 'AphrontView',
+    'PhortuneConstants' => 'Phobject',
     'PhortuneController' => 'PhabricatorController',
+    'PhortuneCreditCardForm' => 'Phobject',
     'PhortuneCurrency' => 'Phobject',
     'PhortuneCurrencySerializer' => 'PhabricatorLiskSerializer',
     'PhortuneCurrencyTestCase' => 'PhabricatorTestCase',
     'PhortuneDAO' => 'PhabricatorLiskDAO',
     'PhortuneErrCode' => 'PhortuneConstants',
     'PhortuneLandingController' => 'PhortuneController',
     'PhortuneMemberHasAccountEdgeType' => 'PhabricatorEdgeType',
     'PhortuneMemberHasMerchantEdgeType' => 'PhabricatorEdgeType',
     'PhortuneMerchant' => array(
       'PhortuneDAO',
       'PhabricatorApplicationTransactionInterface',
       'PhabricatorPolicyInterface',
     ),
     'PhortuneMerchantCapability' => 'PhabricatorPolicyCapability',
     'PhortuneMerchantController' => 'PhortuneController',
     'PhortuneMerchantEditController' => 'PhortuneMerchantController',
     'PhortuneMerchantEditor' => 'PhabricatorApplicationTransactionEditor',
     'PhortuneMerchantHasMemberEdgeType' => 'PhabricatorEdgeType',
     'PhortuneMerchantInvoiceCreateController' => 'PhortuneMerchantController',
     'PhortuneMerchantListController' => 'PhortuneMerchantController',
     'PhortuneMerchantPHIDType' => 'PhabricatorPHIDType',
     'PhortuneMerchantQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'PhortuneMerchantSearchEngine' => 'PhabricatorApplicationSearchEngine',
     'PhortuneMerchantTransaction' => 'PhabricatorApplicationTransaction',
     'PhortuneMerchantTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
     'PhortuneMerchantViewController' => 'PhortuneMerchantController',
     'PhortuneMonthYearExpiryControl' => 'AphrontFormControl',
     'PhortuneNotImplementedException' => 'Exception',
     'PhortuneOrderTableView' => 'AphrontView',
     'PhortunePayPalPaymentProvider' => 'PhortunePaymentProvider',
     'PhortunePaymentMethod' => array(
       'PhortuneDAO',
       'PhabricatorPolicyInterface',
     ),
     'PhortunePaymentMethodCreateController' => 'PhortuneController',
     'PhortunePaymentMethodDisableController' => 'PhortuneController',
     'PhortunePaymentMethodEditController' => 'PhortuneController',
     'PhortunePaymentMethodPHIDType' => 'PhabricatorPHIDType',
     'PhortunePaymentMethodQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
+    'PhortunePaymentProvider' => 'Phobject',
     'PhortunePaymentProviderConfig' => array(
       'PhortuneDAO',
       'PhabricatorPolicyInterface',
     ),
     'PhortunePaymentProviderConfigEditor' => 'PhabricatorApplicationTransactionEditor',
     'PhortunePaymentProviderConfigQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'PhortunePaymentProviderConfigTransaction' => 'PhabricatorApplicationTransaction',
     'PhortunePaymentProviderConfigTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
     'PhortunePaymentProviderPHIDType' => 'PhabricatorPHIDType',
+    'PhortunePaymentProviderTestCase' => 'PhabricatorTestCase',
     'PhortuneProduct' => array(
       'PhortuneDAO',
       'PhabricatorPolicyInterface',
     ),
+    'PhortuneProductImplementation' => 'Phobject',
     'PhortuneProductListController' => 'PhabricatorController',
     'PhortuneProductPHIDType' => 'PhabricatorPHIDType',
     'PhortuneProductQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'PhortuneProductViewController' => 'PhortuneController',
     'PhortuneProviderActionController' => 'PhortuneController',
     'PhortuneProviderDisableController' => 'PhortuneMerchantController',
     'PhortuneProviderEditController' => 'PhortuneMerchantController',
     'PhortunePurchase' => array(
       'PhortuneDAO',
       'PhabricatorPolicyInterface',
     ),
     'PhortunePurchasePHIDType' => 'PhabricatorPHIDType',
     'PhortunePurchaseQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'PhortuneSchemaSpec' => 'PhabricatorConfigSchemaSpec',
     'PhortuneStripePaymentProvider' => 'PhortunePaymentProvider',
     'PhortuneSubscription' => array(
       'PhortuneDAO',
       'PhabricatorPolicyInterface',
     ),
     'PhortuneSubscriptionCart' => 'PhortuneCartImplementation',
     'PhortuneSubscriptionEditController' => 'PhortuneController',
+    'PhortuneSubscriptionImplementation' => 'Phobject',
     'PhortuneSubscriptionListController' => 'PhortuneController',
     'PhortuneSubscriptionPHIDType' => 'PhabricatorPHIDType',
     'PhortuneSubscriptionProduct' => 'PhortuneProductImplementation',
     'PhortuneSubscriptionQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'PhortuneSubscriptionSearchEngine' => 'PhabricatorApplicationSearchEngine',
     'PhortuneSubscriptionTableView' => 'AphrontView',
     'PhortuneSubscriptionViewController' => 'PhortuneController',
     'PhortuneSubscriptionWorker' => 'PhabricatorWorker',
     'PhortuneTestPaymentProvider' => 'PhortunePaymentProvider',
     'PhortuneWePayPaymentProvider' => 'PhortunePaymentProvider',
     'PhragmentBrowseController' => 'PhragmentController',
     'PhragmentCanCreateCapability' => 'PhabricatorPolicyCapability',
     'PhragmentConduitAPIMethod' => 'ConduitAPIMethod',
     'PhragmentController' => 'PhabricatorController',
     'PhragmentCreateController' => 'PhragmentController',
     'PhragmentDAO' => 'PhabricatorLiskDAO',
     'PhragmentFragment' => array(
       'PhragmentDAO',
       'PhabricatorPolicyInterface',
     ),
     'PhragmentFragmentPHIDType' => 'PhabricatorPHIDType',
     'PhragmentFragmentQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'PhragmentFragmentVersion' => array(
       'PhragmentDAO',
       'PhabricatorPolicyInterface',
     ),
     'PhragmentFragmentVersionPHIDType' => 'PhabricatorPHIDType',
     'PhragmentFragmentVersionQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'PhragmentGetPatchConduitAPIMethod' => 'PhragmentConduitAPIMethod',
     'PhragmentHistoryController' => 'PhragmentController',
     'PhragmentPatchController' => 'PhragmentController',
     'PhragmentPatchUtil' => 'Phobject',
     'PhragmentPolicyController' => 'PhragmentController',
     'PhragmentQueryFragmentsConduitAPIMethod' => 'PhragmentConduitAPIMethod',
     'PhragmentRevertController' => 'PhragmentController',
     'PhragmentSchemaSpec' => 'PhabricatorConfigSchemaSpec',
     'PhragmentSnapshot' => array(
       'PhragmentDAO',
       'PhabricatorPolicyInterface',
     ),
     'PhragmentSnapshotChild' => array(
       'PhragmentDAO',
       'PhabricatorPolicyInterface',
     ),
     'PhragmentSnapshotChildQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'PhragmentSnapshotCreateController' => 'PhragmentController',
     'PhragmentSnapshotDeleteController' => 'PhragmentController',
     'PhragmentSnapshotPHIDType' => 'PhabricatorPHIDType',
     'PhragmentSnapshotPromoteController' => 'PhragmentController',
     'PhragmentSnapshotQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'PhragmentSnapshotViewController' => 'PhragmentController',
     'PhragmentUpdateController' => 'PhragmentController',
     'PhragmentVersionController' => 'PhragmentController',
     'PhragmentZIPController' => 'PhragmentController',
     'PhrequentConduitAPIMethod' => 'ConduitAPIMethod',
     'PhrequentController' => 'PhabricatorController',
     'PhrequentDAO' => 'PhabricatorLiskDAO',
     'PhrequentListController' => 'PhrequentController',
     'PhrequentPopConduitAPIMethod' => 'PhrequentConduitAPIMethod',
     'PhrequentPushConduitAPIMethod' => 'PhrequentConduitAPIMethod',
     'PhrequentSearchEngine' => 'PhabricatorApplicationSearchEngine',
     'PhrequentTimeBlock' => 'Phobject',
     'PhrequentTimeBlockTestCase' => 'PhabricatorTestCase',
     'PhrequentTimeSlices' => 'Phobject',
     'PhrequentTrackController' => 'PhrequentController',
     'PhrequentTrackingConduitAPIMethod' => 'PhrequentConduitAPIMethod',
     'PhrequentTrackingEditor' => 'PhabricatorEditor',
     'PhrequentUIEventListener' => 'PhabricatorEventListener',
     'PhrequentUserTime' => array(
       'PhrequentDAO',
       'PhabricatorPolicyInterface',
     ),
     'PhrequentUserTimeQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'PhrictionChangeType' => 'PhrictionConstants',
     'PhrictionConduitAPIMethod' => 'ConduitAPIMethod',
+    'PhrictionConstants' => 'Phobject',
     'PhrictionContent' => array(
       'PhrictionDAO',
       'PhabricatorMarkupInterface',
     ),
     'PhrictionController' => 'PhabricatorController',
     'PhrictionCreateConduitAPIMethod' => 'PhrictionConduitAPIMethod',
     'PhrictionDAO' => 'PhabricatorLiskDAO',
     'PhrictionDeleteController' => 'PhrictionController',
     'PhrictionDiffController' => 'PhrictionController',
     'PhrictionDocument' => array(
       'PhrictionDAO',
       'PhabricatorPolicyInterface',
       'PhabricatorSubscribableInterface',
       'PhabricatorFlaggableInterface',
       'PhabricatorTokenReceiverInterface',
       'PhabricatorDestructibleInterface',
       'PhabricatorApplicationTransactionInterface',
     ),
     'PhrictionDocumentController' => 'PhrictionController',
     'PhrictionDocumentHeraldAdapter' => 'HeraldAdapter',
     'PhrictionDocumentPHIDType' => 'PhabricatorPHIDType',
     'PhrictionDocumentQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'PhrictionDocumentStatus' => 'PhrictionConstants',
     'PhrictionEditConduitAPIMethod' => 'PhrictionConduitAPIMethod',
     'PhrictionEditController' => 'PhrictionController',
     'PhrictionHistoryConduitAPIMethod' => 'PhrictionConduitAPIMethod',
     'PhrictionHistoryController' => 'PhrictionController',
     'PhrictionInfoConduitAPIMethod' => 'PhrictionConduitAPIMethod',
     'PhrictionListController' => 'PhrictionController',
     'PhrictionMoveController' => 'PhrictionController',
     'PhrictionNewController' => 'PhrictionController',
     'PhrictionRemarkupRule' => 'PhutilRemarkupRule',
     'PhrictionReplyHandler' => 'PhabricatorApplicationTransactionReplyHandler',
     'PhrictionSchemaSpec' => 'PhabricatorConfigSchemaSpec',
     'PhrictionSearchEngine' => 'PhabricatorApplicationSearchEngine',
     'PhrictionSearchIndexer' => 'PhabricatorSearchDocumentIndexer',
     'PhrictionTransaction' => 'PhabricatorApplicationTransaction',
     'PhrictionTransactionComment' => 'PhabricatorApplicationTransactionComment',
     'PhrictionTransactionEditor' => 'PhabricatorApplicationTransactionEditor',
     'PhrictionTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
     'PolicyLockOptionType' => 'PhabricatorConfigJSONOptionType',
     'PonderAddAnswerView' => 'AphrontView',
     'PonderAnswer' => array(
       'PonderDAO',
       'PhabricatorApplicationTransactionInterface',
       'PhabricatorMarkupInterface',
       'PonderVotableInterface',
       'PhabricatorPolicyInterface',
       'PhabricatorFlaggableInterface',
       'PhabricatorSubscribableInterface',
       'PhabricatorTokenReceiverInterface',
       'PhabricatorDestructibleInterface',
     ),
     'PonderAnswerCommentController' => 'PonderController',
     'PonderAnswerEditController' => 'PonderController',
     'PonderAnswerEditor' => 'PonderEditor',
     'PonderAnswerHasVotingUserEdgeType' => 'PhabricatorEdgeType',
     'PonderAnswerHistoryController' => 'PonderController',
     'PonderAnswerPHIDType' => 'PhabricatorPHIDType',
     'PonderAnswerQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'PonderAnswerSaveController' => 'PonderController',
     'PonderAnswerTransaction' => 'PhabricatorApplicationTransaction',
     'PonderAnswerTransactionComment' => 'PhabricatorApplicationTransactionComment',
     'PonderAnswerTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
+    'PonderConstants' => 'Phobject',
     'PonderController' => 'PhabricatorController',
     'PonderDAO' => 'PhabricatorLiskDAO',
     'PonderEditor' => 'PhabricatorApplicationTransactionEditor',
     'PonderQuestion' => array(
       'PonderDAO',
       'PhabricatorApplicationTransactionInterface',
       'PhabricatorMarkupInterface',
       'PonderVotableInterface',
       'PhabricatorSubscribableInterface',
       'PhabricatorFlaggableInterface',
       'PhabricatorPolicyInterface',
       'PhabricatorTokenReceiverInterface',
       'PhabricatorProjectInterface',
       'PhabricatorDestructibleInterface',
     ),
     'PonderQuestionCommentController' => 'PonderController',
     'PonderQuestionEditController' => 'PonderController',
     'PonderQuestionEditor' => 'PonderEditor',
     'PonderQuestionHasVotingUserEdgeType' => 'PhabricatorEdgeType',
     'PonderQuestionHistoryController' => 'PonderController',
     'PonderQuestionListController' => 'PonderController',
     'PonderQuestionMailReceiver' => 'PhabricatorObjectMailReceiver',
     'PonderQuestionPHIDType' => 'PhabricatorPHIDType',
     'PonderQuestionQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'PonderQuestionReplyHandler' => 'PhabricatorApplicationTransactionReplyHandler',
     'PonderQuestionSearchEngine' => 'PhabricatorApplicationSearchEngine',
     'PonderQuestionStatus' => 'PonderConstants',
     'PonderQuestionStatusController' => 'PonderController',
     'PonderQuestionTransaction' => 'PhabricatorApplicationTransaction',
     'PonderQuestionTransactionComment' => 'PhabricatorApplicationTransactionComment',
     'PonderQuestionTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
     'PonderQuestionViewController' => 'PonderController',
     'PonderRemarkupRule' => 'PhabricatorObjectRemarkupRule',
     'PonderSchemaSpec' => 'PhabricatorConfigSchemaSpec',
     'PonderSearchIndexer' => 'PhabricatorSearchDocumentIndexer',
     'PonderTransactionFeedStory' => 'PhabricatorApplicationTransactionFeedStory',
     'PonderVotableView' => 'AphrontView',
     'PonderVote' => 'PonderConstants',
     'PonderVoteEditor' => 'PhabricatorEditor',
     'PonderVoteSaveController' => 'PonderController',
     'PonderVotingUserHasAnswerEdgeType' => 'PhabricatorEdgeType',
     'PonderVotingUserHasQuestionEdgeType' => 'PhabricatorEdgeType',
     'ProjectAddProjectsEmailCommand' => 'MetaMTAEmailTransactionCommand',
+    'ProjectBoardTaskCard' => 'Phobject',
     'ProjectCanLockProjectsCapability' => 'PhabricatorPolicyCapability',
     'ProjectConduitAPIMethod' => 'ConduitAPIMethod',
     'ProjectCreateConduitAPIMethod' => 'ProjectConduitAPIMethod',
     'ProjectCreateProjectsCapability' => 'PhabricatorPolicyCapability',
     'ProjectDefaultEditCapability' => 'PhabricatorPolicyCapability',
     'ProjectDefaultJoinCapability' => 'PhabricatorPolicyCapability',
     'ProjectDefaultViewCapability' => 'PhabricatorPolicyCapability',
     'ProjectQueryConduitAPIMethod' => 'ProjectConduitAPIMethod',
     'ProjectRemarkupRule' => 'PhabricatorObjectRemarkupRule',
     'ProjectRemarkupRuleTestCase' => 'PhabricatorTestCase',
     'ProjectReplyHandler' => 'PhabricatorApplicationTransactionReplyHandler',
     'QueryFormattingTestCase' => 'PhabricatorTestCase',
     'ReleephAuthorFieldSpecification' => 'ReleephFieldSpecification',
     'ReleephBranch' => array(
       'ReleephDAO',
       'PhabricatorApplicationTransactionInterface',
       'PhabricatorPolicyInterface',
     ),
     'ReleephBranchAccessController' => 'ReleephBranchController',
     'ReleephBranchCommitFieldSpecification' => 'ReleephFieldSpecification',
     'ReleephBranchController' => 'ReleephController',
     'ReleephBranchCreateController' => 'ReleephProductController',
     'ReleephBranchEditController' => 'ReleephBranchController',
     'ReleephBranchEditor' => 'PhabricatorEditor',
     'ReleephBranchHistoryController' => 'ReleephBranchController',
     'ReleephBranchNamePreviewController' => 'ReleephController',
     'ReleephBranchPHIDType' => 'PhabricatorPHIDType',
     'ReleephBranchPreviewView' => 'AphrontFormControl',
     'ReleephBranchQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'ReleephBranchSearchEngine' => 'PhabricatorApplicationSearchEngine',
+    'ReleephBranchTemplate' => 'Phobject',
     'ReleephBranchTransaction' => 'PhabricatorApplicationTransaction',
     'ReleephBranchTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
     'ReleephBranchViewController' => array(
       'ReleephBranchController',
       'PhabricatorApplicationSearchResultsControllerInterface',
     ),
+    'ReleephCommitFinder' => 'Phobject',
     'ReleephCommitFinderException' => 'Exception',
     'ReleephCommitMessageFieldSpecification' => 'ReleephFieldSpecification',
     'ReleephConduitAPIMethod' => 'ConduitAPIMethod',
     'ReleephController' => 'PhabricatorController',
     'ReleephDAO' => 'PhabricatorLiskDAO',
     'ReleephDefaultFieldSelector' => 'ReleephFieldSelector',
     'ReleephDependsOnFieldSpecification' => 'ReleephFieldSpecification',
     'ReleephDiffChurnFieldSpecification' => 'ReleephFieldSpecification',
     'ReleephDiffMessageFieldSpecification' => 'ReleephFieldSpecification',
     'ReleephDiffSizeFieldSpecification' => 'ReleephFieldSpecification',
     'ReleephFieldParseException' => 'Exception',
+    'ReleephFieldSelector' => 'Phobject',
     'ReleephFieldSpecification' => array(
       'PhabricatorCustomField',
       'PhabricatorMarkupInterface',
     ),
     'ReleephGetBranchesConduitAPIMethod' => 'ReleephConduitAPIMethod',
     'ReleephIntentFieldSpecification' => 'ReleephFieldSpecification',
     'ReleephLevelFieldSpecification' => 'ReleephFieldSpecification',
     'ReleephOriginalCommitFieldSpecification' => 'ReleephFieldSpecification',
     'ReleephProductActionController' => 'ReleephProductController',
     'ReleephProductController' => 'ReleephController',
     'ReleephProductCreateController' => 'ReleephProductController',
     'ReleephProductEditController' => 'ReleephProductController',
     'ReleephProductEditor' => 'PhabricatorApplicationTransactionEditor',
     'ReleephProductHistoryController' => 'ReleephProductController',
     'ReleephProductListController' => 'ReleephController',
     'ReleephProductPHIDType' => 'PhabricatorPHIDType',
     'ReleephProductQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'ReleephProductSearchEngine' => 'PhabricatorApplicationSearchEngine',
     'ReleephProductTransaction' => 'PhabricatorApplicationTransaction',
     'ReleephProductTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
     'ReleephProductViewController' => array(
       'ReleephProductController',
       'PhabricatorApplicationSearchResultsControllerInterface',
     ),
     'ReleephProject' => array(
       'ReleephDAO',
       'PhabricatorApplicationTransactionInterface',
       'PhabricatorPolicyInterface',
     ),
     'ReleephQueryBranchesConduitAPIMethod' => 'ReleephConduitAPIMethod',
     'ReleephQueryProductsConduitAPIMethod' => 'ReleephConduitAPIMethod',
     'ReleephQueryRequestsConduitAPIMethod' => 'ReleephConduitAPIMethod',
     'ReleephReasonFieldSpecification' => 'ReleephFieldSpecification',
     'ReleephRequest' => array(
       'ReleephDAO',
       'PhabricatorApplicationTransactionInterface',
       'PhabricatorPolicyInterface',
       'PhabricatorCustomFieldInterface',
     ),
     'ReleephRequestActionController' => 'ReleephRequestController',
     'ReleephRequestCommentController' => 'ReleephRequestController',
     'ReleephRequestConduitAPIMethod' => 'ReleephConduitAPIMethod',
     'ReleephRequestController' => 'ReleephController',
     'ReleephRequestDifferentialCreateController' => 'ReleephController',
     'ReleephRequestEditController' => 'ReleephBranchController',
     'ReleephRequestMailReceiver' => 'PhabricatorObjectMailReceiver',
     'ReleephRequestPHIDType' => 'PhabricatorPHIDType',
     'ReleephRequestQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'ReleephRequestReplyHandler' => 'PhabricatorApplicationTransactionReplyHandler',
     'ReleephRequestSearchEngine' => 'PhabricatorApplicationSearchEngine',
+    'ReleephRequestStatus' => 'Phobject',
     'ReleephRequestTransaction' => 'PhabricatorApplicationTransaction',
     'ReleephRequestTransactionComment' => 'PhabricatorApplicationTransactionComment',
     'ReleephRequestTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
     'ReleephRequestTransactionalEditor' => 'PhabricatorApplicationTransactionEditor',
     'ReleephRequestTypeaheadControl' => 'AphrontFormControl',
     'ReleephRequestTypeaheadController' => 'PhabricatorTypeaheadDatasourceController',
     'ReleephRequestView' => 'AphrontView',
     'ReleephRequestViewController' => 'ReleephBranchController',
     'ReleephRequestorFieldSpecification' => 'ReleephFieldSpecification',
     'ReleephRevisionFieldSpecification' => 'ReleephFieldSpecification',
     'ReleephSeverityFieldSpecification' => 'ReleephLevelFieldSpecification',
     'ReleephSummaryFieldSpecification' => 'ReleephFieldSpecification',
     'ReleephWorkCanPushConduitAPIMethod' => 'ReleephConduitAPIMethod',
     'ReleephWorkGetAuthorInfoConduitAPIMethod' => 'ReleephConduitAPIMethod',
     'ReleephWorkGetBranchCommitMessageConduitAPIMethod' => 'ReleephConduitAPIMethod',
     'ReleephWorkGetBranchConduitAPIMethod' => 'ReleephConduitAPIMethod',
     'ReleephWorkGetCommitMessageConduitAPIMethod' => 'ReleephConduitAPIMethod',
     'ReleephWorkNextRequestConduitAPIMethod' => 'ReleephConduitAPIMethod',
     'ReleephWorkRecordConduitAPIMethod' => 'ReleephConduitAPIMethod',
     'ReleephWorkRecordPickStatusConduitAPIMethod' => 'ReleephConduitAPIMethod',
     'RemarkupProcessConduitAPIMethod' => 'ConduitAPIMethod',
     'RepositoryConduitAPIMethod' => 'ConduitAPIMethod',
     'RepositoryCreateConduitAPIMethod' => 'RepositoryConduitAPIMethod',
     'RepositoryQueryConduitAPIMethod' => 'RepositoryConduitAPIMethod',
     'ShellLogView' => 'AphrontView',
     'SlowvoteConduitAPIMethod' => 'ConduitAPIMethod',
     'SlowvoteEmbedView' => 'AphrontView',
     'SlowvoteInfoConduitAPIMethod' => 'SlowvoteConduitAPIMethod',
     'SlowvoteRemarkupRule' => 'PhabricatorObjectRemarkupRule',
+    'SubscriptionListDialogBuilder' => 'Phobject',
+    'SubscriptionListStringBuilder' => 'Phobject',
     'TokenConduitAPIMethod' => 'ConduitAPIMethod',
     'TokenGiveConduitAPIMethod' => 'TokenConduitAPIMethod',
     'TokenGivenConduitAPIMethod' => 'TokenConduitAPIMethod',
     'TokenQueryConduitAPIMethod' => 'TokenConduitAPIMethod',
     'UserConduitAPIMethod' => 'ConduitAPIMethod',
     'UserDisableConduitAPIMethod' => 'UserConduitAPIMethod',
     'UserEnableConduitAPIMethod' => 'UserConduitAPIMethod',
     'UserFindConduitAPIMethod' => 'UserConduitAPIMethod',
     'UserQueryConduitAPIMethod' => 'UserConduitAPIMethod',
     'UserWhoAmIConduitAPIMethod' => 'UserConduitAPIMethod',
   ),
 ));
diff --git a/src/aphront/AphrontRequest.php b/src/aphront/AphrontRequest.php
index f4a1ee2cf..d29cd926d 100644
--- a/src/aphront/AphrontRequest.php
+++ b/src/aphront/AphrontRequest.php
@@ -1,766 +1,767 @@
 <?php
 
 /**
  * @task data     Accessing Request Data
  * @task cookie   Managing Cookies
  * @task cluster  Working With a Phabricator Cluster
  */
-final class AphrontRequest {
+final class AphrontRequest extends Phobject {
 
   // NOTE: These magic request-type parameters are automatically included in
   // certain requests (e.g., by phabricator_form(), JX.Request,
   // JX.Workflow, and ConduitClient) and help us figure out what sort of
   // response the client expects.
 
   const TYPE_AJAX = '__ajax__';
   const TYPE_FORM = '__form__';
   const TYPE_CONDUIT = '__conduit__';
   const TYPE_WORKFLOW = '__wflow__';
   const TYPE_CONTINUE = '__continue__';
   const TYPE_PREVIEW = '__preview__';
   const TYPE_HISEC = '__hisec__';
   const TYPE_QUICKSAND = '__quicksand__';
 
   private $host;
   private $path;
   private $requestData;
   private $user;
   private $applicationConfiguration;
   private $uriData;
+  private $cookiePrefix;
 
   public function __construct($host, $path) {
     $this->host = $host;
     $this->path = $path;
   }
 
   public function setURIMap(array $uri_data) {
     $this->uriData = $uri_data;
     return $this;
   }
 
   public function getURIMap() {
     return $this->uriData;
   }
 
   public function getURIData($key, $default = null) {
     return idx($this->uriData, $key, $default);
   }
 
   public function setApplicationConfiguration(
     $application_configuration) {
     $this->applicationConfiguration = $application_configuration;
     return $this;
   }
 
   public function getApplicationConfiguration() {
     return $this->applicationConfiguration;
   }
 
   public function setPath($path) {
     $this->path = $path;
     return $this;
   }
 
   public function getPath() {
     return $this->path;
   }
 
   public function getHost() {
     // The "Host" header may include a port number, or may be a malicious
     // header in the form "realdomain.com:ignored@evil.com". Invoke the full
     // parser to extract the real domain correctly. See here for coverage of
     // a similar issue in Django:
     //
     //  https://www.djangoproject.com/weblog/2012/oct/17/security/
     $uri = new PhutilURI('http://'.$this->host);
     return $uri->getDomain();
   }
 
 
 /* -(  Accessing Request Data  )--------------------------------------------- */
 
 
   /**
    * @task data
    */
   public function setRequestData(array $request_data) {
     $this->requestData = $request_data;
     return $this;
   }
 
 
   /**
    * @task data
    */
   public function getRequestData() {
     return $this->requestData;
   }
 
 
   /**
    * @task data
    */
   public function getInt($name, $default = null) {
     if (isset($this->requestData[$name])) {
       return (int)$this->requestData[$name];
     } else {
       return $default;
     }
   }
 
 
   /**
    * @task data
    */
   public function getBool($name, $default = null) {
     if (isset($this->requestData[$name])) {
       if ($this->requestData[$name] === 'true') {
         return true;
       } else if ($this->requestData[$name] === 'false') {
         return false;
       } else {
         return (bool)$this->requestData[$name];
       }
     } else {
       return $default;
     }
   }
 
 
   /**
    * @task data
    */
   public function getStr($name, $default = null) {
     if (isset($this->requestData[$name])) {
       $str = (string)$this->requestData[$name];
       // Normalize newline craziness.
       $str = str_replace(
         array("\r\n", "\r"),
         array("\n", "\n"),
         $str);
       return $str;
     } else {
       return $default;
     }
   }
 
 
   /**
    * @task data
    */
   public function getArr($name, $default = array()) {
     if (isset($this->requestData[$name]) &&
         is_array($this->requestData[$name])) {
       return $this->requestData[$name];
     } else {
       return $default;
     }
   }
 
 
   /**
    * @task data
    */
   public function getStrList($name, $default = array()) {
     if (!isset($this->requestData[$name])) {
       return $default;
     }
     $list = $this->getStr($name);
     $list = preg_split('/[\s,]+/', $list, $limit = -1, PREG_SPLIT_NO_EMPTY);
     return $list;
   }
 
 
   /**
    * @task data
    */
   public function getExists($name) {
     return array_key_exists($name, $this->requestData);
   }
 
   public function getFileExists($name) {
     return isset($_FILES[$name]) &&
            (idx($_FILES[$name], 'error') !== UPLOAD_ERR_NO_FILE);
   }
 
   public function isHTTPGet() {
     return ($_SERVER['REQUEST_METHOD'] == 'GET');
   }
 
   public function isHTTPPost() {
     return ($_SERVER['REQUEST_METHOD'] == 'POST');
   }
 
   public function isAjax() {
     return $this->getExists(self::TYPE_AJAX) && !$this->isQuicksand();
   }
 
   public function isWorkflow() {
     return $this->getExists(self::TYPE_WORKFLOW) && !$this->isQuicksand();
   }
 
   public function isQuicksand() {
     return $this->getExists(self::TYPE_QUICKSAND);
   }
 
   public function isConduit() {
     return $this->getExists(self::TYPE_CONDUIT);
   }
 
   public static function getCSRFTokenName() {
     return '__csrf__';
   }
 
   public static function getCSRFHeaderName() {
     return 'X-Phabricator-Csrf';
   }
 
   public function validateCSRF() {
     $token_name = self::getCSRFTokenName();
     $token = $this->getStr($token_name);
 
     // No token in the request, check the HTTP header which is added for Ajax
     // requests.
     if (empty($token)) {
       $token = self::getHTTPHeader(self::getCSRFHeaderName());
     }
 
     $valid = $this->getUser()->validateCSRFToken($token);
     if (!$valid) {
 
       // Add some diagnostic details so we can figure out if some CSRF issues
       // are JS problems or people accessing Ajax URIs directly with their
       // browsers.
       $more_info = array();
 
       if ($this->isAjax()) {
         $more_info[] = pht('This was an Ajax request.');
       } else {
         $more_info[] = pht('This was a Web request.');
       }
 
       if ($token) {
         $more_info[] = pht('This request had an invalid CSRF token.');
       } else {
         $more_info[] = pht('This request had no CSRF token.');
       }
 
       // Give a more detailed explanation of how to avoid the exception
       // in developer mode.
       if (PhabricatorEnv::getEnvConfig('phabricator.developer-mode')) {
         // TODO: Clean this up, see T1921.
         $more_info[] = pht(
           "To avoid this error, use %s to construct forms. If you are already ".
           "using %s, make sure the form 'action' uses a relative URI (i.e., ".
           "begins with a '%s'). Forms using absolute URIs do not include CSRF ".
           "tokens, to prevent leaking tokens to external sites.\n\n".
           "If this page performs writes which do not require CSRF protection ".
           "(usually, filling caches or logging), you can use %s to ".
           "temporarily bypass CSRF protection while writing. You should use ".
           "this only for writes which can not be protected with normal CSRF ".
           "mechanisms.\n\n".
           "Some UI elements (like %s) also have methods which will allow you ".
           "to render links as forms (like %s).",
           'phabricator_form()',
           'phabricator_form()',
           '/',
           'AphrontWriteGuard::beginScopedUnguardedWrites()',
           'PhabricatorActionListView',
           'setRenderAsForm(true)');
       }
 
       // This should only be able to happen if you load a form, pull your
       // internet for 6 hours, and then reconnect and immediately submit,
       // but give the user some indication of what happened since the workflow
       // is incredibly confusing otherwise.
       throw new AphrontCSRFException(
         pht(
           'You are trying to save some data to Phabricator, but the request '.
           'your browser made included an incorrect token. Reload the page '.
           'and try again. You may need to clear your cookies.')."\n\n".
           implode("\n", $more_info));
     }
 
     return true;
   }
 
   public function isFormPost() {
     $post = $this->getExists(self::TYPE_FORM) &&
             !$this->getExists(self::TYPE_HISEC) &&
             $this->isHTTPPost();
 
     if (!$post) {
       return false;
     }
 
     return $this->validateCSRF();
   }
 
   public function isFormOrHisecPost() {
     $post = $this->getExists(self::TYPE_FORM) &&
             $this->isHTTPPost();
 
     if (!$post) {
       return false;
     }
 
     return $this->validateCSRF();
   }
 
 
   public function setCookiePrefix($prefix) {
     $this->cookiePrefix = $prefix;
     return $this;
   }
 
   private function getPrefixedCookieName($name) {
     if (strlen($this->cookiePrefix)) {
       return $this->cookiePrefix.'_'.$name;
     } else {
       return $name;
     }
   }
 
   public function getCookie($name, $default = null) {
     $name = $this->getPrefixedCookieName($name);
     $value = idx($_COOKIE, $name, $default);
 
     // Internally, PHP deletes cookies by setting them to the value 'deleted'
     // with an expiration date in the past.
 
     // At least in Safari, the browser may send this cookie anyway in some
     // circumstances. After logging out, the 302'd GET to /login/ consistently
     // includes deleted cookies on my local install. If a cookie value is
     // literally 'deleted', pretend it does not exist.
 
     if ($value === 'deleted') {
       return null;
     }
 
     return $value;
   }
 
   public function clearCookie($name) {
     $this->setCookieWithExpiration($name, '', time() - (60 * 60 * 24 * 30));
     unset($_COOKIE[$name]);
   }
 
   /**
    * Get the domain which cookies should be set on for this request, or null
    * if the request does not correspond to a valid cookie domain.
    *
    * @return PhutilURI|null   Domain URI, or null if no valid domain exists.
    *
    * @task cookie
    */
   private function getCookieDomainURI() {
     if (PhabricatorEnv::getEnvConfig('security.require-https') &&
         !$this->isHTTPS()) {
       return null;
     }
 
     $host = $this->getHost();
 
     // If there's no base domain configured, just use whatever the request
     // domain is. This makes setup easier, and we'll tell administrators to
     // configure a base domain during the setup process.
     $base_uri = PhabricatorEnv::getEnvConfig('phabricator.base-uri');
     if (!strlen($base_uri)) {
       return new PhutilURI('http://'.$host.'/');
     }
 
     $alternates = PhabricatorEnv::getEnvConfig('phabricator.allowed-uris');
     $allowed_uris = array_merge(
       array($base_uri),
       $alternates);
 
     foreach ($allowed_uris as $allowed_uri) {
       $uri = new PhutilURI($allowed_uri);
       if ($uri->getDomain() == $host) {
         return $uri;
       }
     }
 
     return null;
   }
 
   /**
    * Determine if security policy rules will allow cookies to be set when
    * responding to the request.
    *
    * @return bool True if setCookie() will succeed. If this method returns
    *              false, setCookie() will throw.
    *
    * @task cookie
    */
   public function canSetCookies() {
     return (bool)$this->getCookieDomainURI();
   }
 
 
   /**
    * Set a cookie which does not expire for a long time.
    *
    * To set a temporary cookie, see @{method:setTemporaryCookie}.
    *
    * @param string  Cookie name.
    * @param string  Cookie value.
    * @return this
    * @task cookie
    */
   public function setCookie($name, $value) {
     $far_future = time() + (60 * 60 * 24 * 365 * 5);
     return $this->setCookieWithExpiration($name, $value, $far_future);
   }
 
 
   /**
    * Set a cookie which expires soon.
    *
    * To set a durable cookie, see @{method:setCookie}.
    *
    * @param string  Cookie name.
    * @param string  Cookie value.
    * @return this
    * @task cookie
    */
   public function setTemporaryCookie($name, $value) {
     return $this->setCookieWithExpiration($name, $value, 0);
   }
 
 
   /**
    * Set a cookie with a given expiration policy.
    *
    * @param string  Cookie name.
    * @param string  Cookie value.
    * @param int     Epoch timestamp for cookie expiration.
    * @return this
    * @task cookie
    */
   private function setCookieWithExpiration(
     $name,
     $value,
     $expire) {
 
     $is_secure = false;
 
     $base_domain_uri = $this->getCookieDomainURI();
     if (!$base_domain_uri) {
       $configured_as = PhabricatorEnv::getEnvConfig('phabricator.base-uri');
       $accessed_as = $this->getHost();
 
       throw new Exception(
         pht(
           'This Phabricator install is configured as "%s", but you are '.
           'using the domain name "%s" to access a page which is trying to '.
           'set a cookie. Acccess Phabricator on the configured primary '.
           'domain or a configured alternate domain. Phabricator will not '.
           'set cookies on other domains for security reasons.',
           $configured_as,
           $accessed_as));
     }
 
     $base_domain = $base_domain_uri->getDomain();
     $is_secure = ($base_domain_uri->getProtocol() == 'https');
 
     $name = $this->getPrefixedCookieName($name);
 
     if (php_sapi_name() == 'cli') {
       // Do nothing, to avoid triggering "Cannot modify header information"
       // warnings.
 
       // TODO: This is effectively a test for whether we're running in a unit
       // test or not. Move this actual call to HTTPSink?
     } else {
       setcookie(
         $name,
         $value,
         $expire,
         $path = '/',
         $base_domain,
         $is_secure,
         $http_only = true);
     }
 
     $_COOKIE[$name] = $value;
 
     return $this;
   }
 
   public function setUser($user) {
     $this->user = $user;
     return $this;
   }
 
   public function getUser() {
     return $this->user;
   }
 
   public function getViewer() {
     return $this->user;
   }
 
   public function getRequestURI() {
     $get = $_GET;
     unset($get['__path__']);
     $path = phutil_escape_uri($this->getPath());
     return id(new PhutilURI($path))->setQueryParams($get);
   }
 
   public function isDialogFormPost() {
     return $this->isFormPost() && $this->getStr('__dialog__');
   }
 
   public function getRemoteAddr() {
     return $_SERVER['REMOTE_ADDR'];
   }
 
   public function isHTTPS() {
     if (empty($_SERVER['HTTPS'])) {
       return false;
     }
     if (!strcasecmp($_SERVER['HTTPS'], 'off')) {
       return false;
     }
     return true;
   }
 
   public function isContinueRequest() {
     return $this->isFormPost() && $this->getStr('__continue__');
   }
 
   public function isPreviewRequest() {
     return $this->isFormPost() && $this->getStr('__preview__');
   }
 
   /**
    * Get application request parameters in a flattened form suitable for
    * inclusion in an HTTP request, excluding parameters with special meanings.
    * This is primarily useful if you want to ask the user for more input and
    * then resubmit their request.
    *
    * @return  dict<string, string>  Original request parameters.
    */
   public function getPassthroughRequestParameters($include_quicksand = false) {
     return self::flattenData(
       $this->getPassthroughRequestData($include_quicksand));
   }
 
   /**
    * Get request data other than "magic" parameters.
    *
    * @return dict<string, wild> Request data, with magic filtered out.
    */
   public function getPassthroughRequestData($include_quicksand = false) {
     $data = $this->getRequestData();
 
     // Remove magic parameters like __dialog__ and __ajax__.
     foreach ($data as $key => $value) {
       if ($include_quicksand && $key == self::TYPE_QUICKSAND) {
         continue;
       }
       if (!strncmp($key, '__', 2)) {
         unset($data[$key]);
       }
     }
 
     return $data;
   }
 
 
   /**
    * Flatten an array of key-value pairs (possibly including arrays as values)
    * into a list of key-value pairs suitable for submitting via HTTP request
    * (with arrays flattened).
    *
    * @param   dict<string, wild>    Data to flatten.
    * @return  dict<string, string>  Flat data suitable for inclusion in an HTTP
    *                                request.
    */
   public static function flattenData(array $data) {
     $result = array();
     foreach ($data as $key => $value) {
       if (is_array($value)) {
         foreach (self::flattenData($value) as $fkey => $fvalue) {
           $fkey = '['.preg_replace('/(?=\[)|$/', ']', $fkey, $limit = 1);
           $result[$key.$fkey] = $fvalue;
         }
       } else {
         $result[$key] = (string)$value;
       }
     }
 
     ksort($result);
 
     return $result;
   }
 
 
   /**
    * Read the value of an HTTP header from `$_SERVER`, or a similar datasource.
    *
    * This function accepts a canonical header name, like `"Accept-Encoding"`,
    * and looks up the appropriate value in `$_SERVER` (in this case,
    * `"HTTP_ACCEPT_ENCODING"`).
    *
    * @param   string        Canonical header name, like `"Accept-Encoding"`.
    * @param   wild          Default value to return if header is not present.
    * @param   array?        Read this instead of `$_SERVER`.
    * @return  string|wild   Header value if present, or `$default` if not.
    */
   public static function getHTTPHeader($name, $default = null, $data = null) {
     // PHP mangles HTTP headers by uppercasing them and replacing hyphens with
     // underscores, then prepending 'HTTP_'.
     $php_index = strtoupper($name);
     $php_index = str_replace('-', '_', $php_index);
 
     $try_names = array();
 
     $try_names[] = 'HTTP_'.$php_index;
     if ($php_index == 'CONTENT_TYPE' || $php_index == 'CONTENT_LENGTH') {
       // These headers may be available under alternate names. See
       // http://www.php.net/manual/en/reserved.variables.server.php#110763
       $try_names[] = $php_index;
     }
 
     if ($data === null) {
       $data = $_SERVER;
     }
 
     foreach ($try_names as $try_name) {
       if (array_key_exists($try_name, $data)) {
         return $data[$try_name];
       }
     }
 
     return $default;
   }
 
 
 /* -(  Working With a Phabricator Cluster  )--------------------------------- */
 
 
   /**
    * Is this a proxied request originating from within the Phabricator cluster?
    *
    * IMPORTANT: This means the request is dangerous!
    *
    * These requests are **more dangerous** than normal requests (they can not
    * be safely proxied, because proxying them may cause a loop). Cluster
    * requests are not guaranteed to come from a trusted source, and should
    * never be treated as safer than normal requests. They are strictly less
    * safe.
    */
   public function isProxiedClusterRequest() {
     return (bool)self::getHTTPHeader('X-Phabricator-Cluster');
   }
 
 
   /**
    * Build a new @{class:HTTPSFuture} which proxies this request to another
    * node in the cluster.
    *
    * IMPORTANT: This is very dangerous!
    *
    * The future forwards authentication information present in the request.
    * Proxied requests must only be sent to trusted hosts. (We attempt to
    * enforce this.)
    *
    * This is not a general-purpose proxying method; it is a specialized
    * method with niche applications and severe security implications.
    *
    * @param string URI identifying the host we are proxying the request to.
    * @return HTTPSFuture New proxy future.
    *
    * @phutil-external-symbol class PhabricatorStartup
    */
   public function newClusterProxyFuture($uri) {
     $uri = new PhutilURI($uri);
 
     $domain = $uri->getDomain();
     $ip = gethostbyname($domain);
     if (!$ip) {
       throw new Exception(
         pht(
           'Unable to resolve domain "%s"!',
           $domain));
     }
 
     if (!PhabricatorEnv::isClusterAddress($ip)) {
       throw new Exception(
         pht(
           'Refusing to proxy a request to IP address ("%s") which is not '.
           'in the cluster address block (this address was derived by '.
           'resolving the domain "%s").',
           $ip,
           $domain));
     }
 
     $uri->setPath($this->getPath());
     $uri->setQueryParams(self::flattenData($_GET));
 
     $input = PhabricatorStartup::getRawInput();
 
     $future = id(new HTTPSFuture($uri))
       ->addHeader('Host', self::getHost())
       ->addHeader('X-Phabricator-Cluster', true)
       ->setMethod($_SERVER['REQUEST_METHOD'])
       ->write($input);
 
     if (isset($_SERVER['PHP_AUTH_USER'])) {
       $future->setHTTPBasicAuthCredentials(
         $_SERVER['PHP_AUTH_USER'],
         new PhutilOpaqueEnvelope(idx($_SERVER, 'PHP_AUTH_PW', '')));
     }
 
     $headers = array();
     $seen = array();
 
     // NOTE: apache_request_headers() might provide a nicer way to do this,
     // but isn't available under FCGI until PHP 5.4.0.
     foreach ($_SERVER as $key => $value) {
       if (preg_match('/^HTTP_/', $key)) {
         // Unmangle the header as best we can.
         $key = str_replace('_', ' ', $key);
         $key = strtolower($key);
         $key = ucwords($key);
         $key = str_replace(' ', '-', $key);
 
         $headers[] = array($key, $value);
         $seen[$key] = true;
       }
     }
 
     // In some situations, this may not be mapped into the HTTP_X constants.
     // CONTENT_LENGTH is similarly affected, but we trust cURL to take care
     // of that if it matters, since we're handing off a request body.
     if (empty($seen['Content-Type'])) {
       if (isset($_SERVER['CONTENT_TYPE'])) {
         $headers[] = array('Content-Type', $_SERVER['CONTENT_TYPE']);
       }
     }
 
     foreach ($headers as $header) {
       list($key, $value) = $header;
       switch ($key) {
         case 'Host':
         case 'Authorization':
           // Don't forward these headers, we've already handled them elsewhere.
           unset($headers[$key]);
           break;
         default:
           break;
       }
     }
 
     foreach ($headers as $header) {
       list($key, $value) = $header;
       $future->addHeader($key, $value);
     }
 
     return $future;
   }
 
 
 }
diff --git a/src/aphront/AphrontURIMapper.php b/src/aphront/AphrontURIMapper.php
index 32f464911..13108729b 100644
--- a/src/aphront/AphrontURIMapper.php
+++ b/src/aphront/AphrontURIMapper.php
@@ -1,50 +1,50 @@
 <?php
 
-final class AphrontURIMapper {
+final class AphrontURIMapper extends Phobject {
 
   private $map;
 
   public function __construct(array $map) {
     $this->map = $map;
   }
 
   public function mapPath($path) {
     $map = $this->map;
     foreach ($map as $rule => $value) {
       list($controller, $data) = $this->tryRule($rule, $value, $path);
       if ($controller) {
         foreach ($data as $k => $v) {
           if (is_numeric($k)) {
             unset($data[$k]);
           }
         }
         return array($controller, $data);
       }
     }
 
     return array(null, null);
   }
 
   private function tryRule($rule, $value, $path) {
     $match = null;
     $pattern = '#^'.$rule.(is_array($value) ? '' : '$').'#';
     if (!preg_match($pattern, $path, $match)) {
       return array(null, null);
     }
 
     if (!is_array($value)) {
       return array($value, $match);
     }
 
     $path = substr($path, strlen($match[0]));
     foreach ($value as $srule => $sval) {
       list($controller, $data) = $this->tryRule($srule, $sval, $path);
       if ($controller) {
         return array($controller, $data + $match);
       }
     }
 
     return array(null, null);
   }
 
 }
diff --git a/src/aphront/configuration/AphrontApplicationConfiguration.php b/src/aphront/configuration/AphrontApplicationConfiguration.php
index d1ced8a3e..4ede4161a 100644
--- a/src/aphront/configuration/AphrontApplicationConfiguration.php
+++ b/src/aphront/configuration/AphrontApplicationConfiguration.php
@@ -1,512 +1,512 @@
 <?php
 
 /**
  * @task  routing URI Routing
  */
-abstract class AphrontApplicationConfiguration {
+abstract class AphrontApplicationConfiguration extends Phobject {
 
   private $request;
   private $host;
   private $path;
   private $console;
 
   abstract public function getApplicationName();
   abstract public function buildRequest();
   abstract public function build404Controller();
   abstract public function buildRedirectController($uri, $external);
 
   final public function setRequest(AphrontRequest $request) {
     $this->request = $request;
     return $this;
   }
 
   final public function getRequest() {
     return $this->request;
   }
 
   final public function getConsole() {
     return $this->console;
   }
 
   final public function setConsole($console) {
     $this->console = $console;
     return $this;
   }
 
   final public function setHost($host) {
     $this->host = $host;
     return $this;
   }
 
   final public function getHost() {
     return $this->host;
   }
 
   final public function setPath($path) {
     $this->path = $path;
     return $this;
   }
 
   final public function getPath() {
     return $this->path;
   }
 
   public function willBuildRequest() {}
 
 
   /**
    * @phutil-external-symbol class PhabricatorStartup
    */
   public static function runHTTPRequest(AphrontHTTPSink $sink) {
     $multimeter = MultimeterControl::newInstance();
     $multimeter->setEventContext('<http-init>');
     $multimeter->setEventViewer('<none>');
 
     // Build a no-op write guard for the setup phase. We'll replace this with a
     // real write guard later on, but we need to survive setup and build a
     // request object first.
     $write_guard = new AphrontWriteGuard('id');
 
     PhabricatorEnv::initializeWebEnvironment();
 
     $multimeter->setSampleRate(
       PhabricatorEnv::getEnvConfig('debug.sample-rate'));
 
     $debug_time_limit = PhabricatorEnv::getEnvConfig('debug.time-limit');
     if ($debug_time_limit) {
       PhabricatorStartup::setDebugTimeLimit($debug_time_limit);
     }
 
     // This is the earliest we can get away with this, we need env config first.
     PhabricatorAccessLog::init();
     $access_log = PhabricatorAccessLog::getLog();
     PhabricatorStartup::setAccessLog($access_log);
     $access_log->setData(
       array(
         'R' => AphrontRequest::getHTTPHeader('Referer', '-'),
         'r' => idx($_SERVER, 'REMOTE_ADDR', '-'),
         'M' => idx($_SERVER, 'REQUEST_METHOD', '-'),
       ));
 
     DarkConsoleXHProfPluginAPI::hookProfiler();
     DarkConsoleErrorLogPluginAPI::registerErrorHandler();
 
     $response = PhabricatorSetupCheck::willProcessRequest();
     if ($response) {
       PhabricatorStartup::endOutputCapture();
       $sink->writeResponse($response);
       return;
     }
 
     $host = AphrontRequest::getHTTPHeader('Host');
     $path = $_REQUEST['__path__'];
 
     switch ($host) {
       default:
         $config_key = 'aphront.default-application-configuration-class';
         $application = PhabricatorEnv::newObjectFromConfig($config_key);
         break;
     }
 
     $application->setHost($host);
     $application->setPath($path);
     $application->willBuildRequest();
     $request = $application->buildRequest();
 
     // Now that we have a request, convert the write guard into one which
     // actually checks CSRF tokens.
     $write_guard->dispose();
     $write_guard = new AphrontWriteGuard(array($request, 'validateCSRF'));
 
     // Build the server URI implied by the request headers. If an administrator
     // has not configured "phabricator.base-uri" yet, we'll use this to generate
     // links.
 
     $request_protocol = ($request->isHTTPS() ? 'https' : 'http');
     $request_base_uri = "{$request_protocol}://{$host}/";
     PhabricatorEnv::setRequestBaseURI($request_base_uri);
 
     $access_log->setData(
       array(
         'U' => (string)$request->getRequestURI()->getPath(),
       ));
 
     $processing_exception = null;
     try {
       $response = $application->processRequest(
         $request,
         $access_log,
         $sink,
         $multimeter);
       $response_code = $response->getHTTPResponseCode();
     } catch (Exception $ex) {
       $processing_exception = $ex;
       $response_code = 500;
     }
 
     $write_guard->dispose();
 
     $access_log->setData(
       array(
         'c' => $response_code,
         'T' => PhabricatorStartup::getMicrosecondsSinceStart(),
       ));
 
     $multimeter->newEvent(
       MultimeterEvent::TYPE_REQUEST_TIME,
       $multimeter->getEventContext(),
       PhabricatorStartup::getMicrosecondsSinceStart());
 
     $access_log->write();
 
     $multimeter->saveEvents();
 
     DarkConsoleXHProfPluginAPI::saveProfilerSample($access_log);
 
     // Add points to the rate limits for this request.
     if (isset($_SERVER['REMOTE_ADDR'])) {
       $user_ip = $_SERVER['REMOTE_ADDR'];
 
       // The base score for a request allows users to make 30 requests per
       // minute.
       $score = (1000 / 30);
 
       // If the user was logged in, let them make more requests.
       if ($request->getUser() && $request->getUser()->getPHID()) {
         $score = $score / 5;
       }
 
       PhabricatorStartup::addRateLimitScore($user_ip, $score);
     }
 
     if ($processing_exception) {
       throw $processing_exception;
     }
   }
 
 
   public function processRequest(
     AphrontRequest $request,
     PhutilDeferredLog $access_log,
     AphrontHTTPSink $sink,
     MultimeterControl $multimeter) {
 
     $this->setRequest($request);
 
     list($controller, $uri_data) = $this->buildController();
 
     $controller_class = get_class($controller);
     $access_log->setData(
       array(
         'C' => $controller_class,
       ));
     $multimeter->setEventContext('web.'.$controller_class);
 
     $request->setURIMap($uri_data);
     $controller->setRequest($request);
 
     // If execution throws an exception and then trying to render that
     // exception throws another exception, we want to show the original
     // exception, as it is likely the root cause of the rendering exception.
     $original_exception = null;
     try {
       $response = $controller->willBeginExecution();
 
       if ($request->getUser() && $request->getUser()->getPHID()) {
         $access_log->setData(
           array(
             'u' => $request->getUser()->getUserName(),
             'P' => $request->getUser()->getPHID(),
           ));
         $multimeter->setEventViewer('user.'.$request->getUser()->getPHID());
       }
 
       if (!$response) {
         $controller->willProcessRequest($uri_data);
         $response = $controller->handleRequest($request);
       }
     } catch (Exception $ex) {
       $original_exception = $ex;
       $response = $this->handleException($ex);
     }
 
     try {
       $response = $controller->didProcessRequest($response);
       $response = $this->willSendResponse($response, $controller);
       $response->setRequest($request);
 
       $unexpected_output = PhabricatorStartup::endOutputCapture();
       if ($unexpected_output) {
         $unexpected_output = pht(
           "Unexpected output:\n\n%s",
           $unexpected_output);
 
         phlog($unexpected_output);
 
         if ($response instanceof AphrontWebpageResponse) {
           echo phutil_tag(
             'div',
             array('style' =>
               'background: #eeddff;'.
               'white-space: pre-wrap;'.
               'z-index: 200000;'.
               'position: relative;'.
               'padding: 8px;'.
               'font-family: monospace',
             ),
             $unexpected_output);
         }
       }
 
       $sink->writeResponse($response);
     } catch (Exception $ex) {
       if ($original_exception) {
         throw $original_exception;
       }
       throw $ex;
     }
 
     return $response;
   }
 
 
 /* -(  URI Routing  )-------------------------------------------------------- */
 
 
   /**
    * Using builtin and application routes, build the appropriate
    * @{class:AphrontController} class for the request. To route a request, we
    * first test if the HTTP_HOST is configured as a valid Phabricator URI. If
    * it isn't, we do a special check to see if it's a custom domain for a blog
    * in the Phame application and if that fails we error. Otherwise, we test
    * against all application routes from installed
    * @{class:PhabricatorApplication}s.
    *
    * If we match a route, we construct the controller it points at, build it,
    * and return it.
    *
    * If we fail to match a route, but the current path is missing a trailing
    * "/", we try routing the same path with a trailing "/" and do a redirect
    * if that has a valid route. The idea is to canoncalize URIs for consistency,
    * but avoid breaking noncanonical URIs that we can easily salvage.
    *
    * NOTE: We only redirect on GET. On POST, we'd drop parameters and most
    * likely mutate the request implicitly, and a bad POST usually indicates a
    * programming error rather than a sloppy typist.
    *
    * If the failing path already has a trailing "/", or we can't route the
    * version with a "/", we call @{method:build404Controller}, which build a
    * fallback @{class:AphrontController}.
    *
    * @return pair<AphrontController,dict> Controller and dictionary of request
    *                                      parameters.
    * @task routing
    */
   final public function buildController() {
     $request = $this->getRequest();
 
     // If we're configured to operate in cluster mode, reject requests which
     // were not received on a cluster interface.
     //
     // For example, a host may have an internal address like "170.0.0.1", and
     // also have a public address like "51.23.95.16". Assuming the cluster
     // is configured on a range like "170.0.0.0/16", we want to reject the
     // requests received on the public interface.
     //
     // Ideally, nodes in a cluster should only be listening on internal
     // interfaces, but they may be configured in such a way that they also
     // listen on external interfaces, since this is easy to forget about or
     // get wrong. As a broad security measure, reject requests received on any
     // interfaces which aren't on the whitelist.
 
     $cluster_addresses = PhabricatorEnv::getEnvConfig('cluster.addresses');
     if ($cluster_addresses) {
       $server_addr = idx($_SERVER, 'SERVER_ADDR');
       if (!$server_addr) {
         if (php_sapi_name() == 'cli') {
           // This is a command line script (probably something like a unit
           // test) so it's fine that we don't have SERVER_ADDR defined.
         } else {
           throw new AphrontUsageException(
             pht('No %s', 'SERVER_ADDR'),
             pht(
               'Phabricator is configured to operate in cluster mode, but '.
               '%s is not defined in the request context. Your webserver '.
               'configuration needs to forward %s to PHP so Phabricator can '.
               'reject requests received on external interfaces.',
               'SERVER_ADDR',
               'SERVER_ADDR'));
         }
       } else {
         if (!PhabricatorEnv::isClusterAddress($server_addr)) {
           throw new AphrontUsageException(
             pht('External Interface'),
             pht(
               'Phabricator is configured in cluster mode and the address '.
               'this request was received on ("%s") is not whitelisted as '.
               'a cluster address.',
               $server_addr));
         }
       }
     }
 
     if (PhabricatorEnv::getEnvConfig('security.require-https')) {
       if (!$request->isHTTPS()) {
         $https_uri = $request->getRequestURI();
         $https_uri->setDomain($request->getHost());
         $https_uri->setProtocol('https');
 
         // In this scenario, we'll be redirecting to HTTPS using an absolute
         // URI, so we need to permit an external redirect.
         return $this->buildRedirectController($https_uri, true);
       }
     }
 
     $path         = $request->getPath();
     $host         = $request->getHost();
     $base_uri     = PhabricatorEnv::getEnvConfig('phabricator.base-uri');
     $prod_uri     = PhabricatorEnv::getEnvConfig('phabricator.production-uri');
     $file_uri     = PhabricatorEnv::getEnvConfig(
       'security.alternate-file-domain');
     $allowed_uris = PhabricatorEnv::getEnvConfig('phabricator.allowed-uris');
 
     $uris = array_merge(
       array(
         $base_uri,
         $prod_uri,
       ),
       $allowed_uris);
 
     $cdn_routes = array(
       '/res/',
       '/file/data/',
       '/file/xform/',
       '/phame/r/',
       );
 
     $host_match = false;
     foreach ($uris as $uri) {
       if ($host === id(new PhutilURI($uri))->getDomain()) {
         $host_match = true;
         break;
       }
     }
 
     if (!$host_match) {
       if ($host === id(new PhutilURI($file_uri))->getDomain()) {
         foreach ($cdn_routes as $route) {
           if (strncmp($path, $route, strlen($route)) == 0) {
             $host_match = true;
             break;
           }
         }
       }
     }
 
     // NOTE: If the base URI isn't defined yet, don't activate alternate
     // domains.
     if ($base_uri && !$host_match) {
 
       try {
         $blog = id(new PhameBlogQuery())
           ->setViewer(new PhabricatorUser())
           ->withDomain($host)
           ->executeOne();
       } catch (PhabricatorPolicyException $ex) {
         throw new Exception(
           pht(
             'This blog is not visible to logged out users, so it can not be '.
             'visited from a custom domain.'));
       }
 
       if (!$blog) {
         if ($prod_uri && $prod_uri != $base_uri) {
           $prod_str = pht('%s or %s', $base_uri, $prod_uri);
         } else {
           $prod_str = $base_uri;
         }
         throw new Exception(
           pht(
             'Specified domain %s is not configured for Phabricator '.
             'requests. Please use %s to visit this instance.',
             $host,
             $prod_str));
       }
 
       // TODO: Make this more flexible and modular so any application can
       // do crazy stuff here if it wants.
 
       $path = '/phame/live/'.$blog->getID().'/'.$path;
     }
 
     list($controller, $uri_data) = $this->buildControllerForPath($path);
     if (!$controller) {
       if (!preg_match('@/$@', $path)) {
         // If we failed to match anything but don't have a trailing slash, try
         // to add a trailing slash and issue a redirect if that resolves.
         list($controller, $uri_data) = $this->buildControllerForPath($path.'/');
 
         // NOTE: For POST, just 404 instead of redirecting, since the redirect
         // will be a GET without parameters.
 
         if ($controller && !$request->isHTTPPost()) {
           $slash_uri = $request->getRequestURI()->setPath($path.'/');
 
           $external = strlen($request->getRequestURI()->getDomain());
           return $this->buildRedirectController($slash_uri, $external);
         }
       }
       return $this->build404Controller();
     }
 
     return array($controller, $uri_data);
   }
 
 
   /**
    * Map a specific path to the corresponding controller. For a description
    * of routing, see @{method:buildController}.
    *
    * @return pair<AphrontController,dict> Controller and dictionary of request
    *                                      parameters.
    * @task routing
    */
   final public function buildControllerForPath($path) {
     $maps = array();
 
     $applications = PhabricatorApplication::getAllInstalledApplications();
     foreach ($applications as $application) {
       $maps[] = array($application, $application->getRoutes());
     }
 
     $current_application = null;
     $controller_class = null;
     foreach ($maps as $map_info) {
       list($application, $map) = $map_info;
 
       $mapper = new AphrontURIMapper($map);
       list($controller_class, $uri_data) = $mapper->mapPath($path);
 
       if ($controller_class) {
         if ($application) {
           $current_application = $application;
         }
         break;
       }
     }
 
     if (!$controller_class) {
       return array(null, null);
     }
 
     $request = $this->getRequest();
 
     $controller = newv($controller_class, array());
     if ($current_application) {
       $controller->setCurrentApplication($current_application);
     }
 
     return array($controller, $uri_data);
   }
 
 }
diff --git a/src/aphront/response/AphrontPlainTextResponse.php b/src/aphront/response/AphrontPlainTextResponse.php
index a14b7367e..d8289ab2f 100644
--- a/src/aphront/response/AphrontPlainTextResponse.php
+++ b/src/aphront/response/AphrontPlainTextResponse.php
@@ -1,22 +1,24 @@
 <?php
 
 final class AphrontPlainTextResponse extends AphrontResponse {
 
+  private $content;
+
   public function setContent($content) {
     $this->content = $content;
     return $this;
   }
 
   public function buildResponseString() {
     return $this->content;
   }
 
   public function getHeaders() {
     $headers = array(
       array('Content-Type', 'text/plain; charset=utf-8'),
     );
 
     return array_merge(parent::getHeaders(), $headers);
   }
 
 }
diff --git a/src/aphront/response/AphrontResponse.php b/src/aphront/response/AphrontResponse.php
index b1e72f552..72dacf977 100644
--- a/src/aphront/response/AphrontResponse.php
+++ b/src/aphront/response/AphrontResponse.php
@@ -1,233 +1,233 @@
 <?php
 
-abstract class AphrontResponse {
+abstract class AphrontResponse extends Phobject {
 
   private $request;
   private $cacheable = false;
   private $responseCode = 200;
   private $lastModified = null;
 
   protected $frameable;
 
   public function setRequest($request) {
     $this->request = $request;
     return $this;
   }
 
   public function getRequest() {
     return $this->request;
   }
 
 
 /* -(  Content  )------------------------------------------------------------ */
 
 
   public function getContentIterator() {
     return array($this->buildResponseString());
   }
 
   public function buildResponseString() {
     throw new PhutilMethodNotImplementedException();
   }
 
 
 /* -(  Metadata  )----------------------------------------------------------- */
 
 
   public function getHeaders() {
     $headers = array();
     if (!$this->frameable) {
       $headers[] = array('X-Frame-Options', 'Deny');
     }
 
     if ($this->getRequest() && $this->getRequest()->isHTTPS()) {
       $hsts_key = 'security.strict-transport-security';
       $use_hsts = PhabricatorEnv::getEnvConfig($hsts_key);
       if ($use_hsts) {
         $duration = phutil_units('365 days in seconds');
       } else {
         // If HSTS has been disabled, tell browsers to turn it off. This may
         // not be effective because we can only disable it over a valid HTTPS
         // connection, but it best represents the configured intent.
         $duration = 0;
       }
 
       $headers[] = array(
         'Strict-Transport-Security',
         "max-age={$duration}; includeSubdomains; preload",
       );
     }
 
     return $headers;
   }
 
   public function setCacheDurationInSeconds($duration) {
     $this->cacheable = $duration;
     return $this;
   }
 
   public function setLastModified($epoch_timestamp) {
     $this->lastModified = $epoch_timestamp;
     return $this;
   }
 
   public function setHTTPResponseCode($code) {
     $this->responseCode = $code;
     return $this;
   }
 
   public function getHTTPResponseCode() {
     return $this->responseCode;
   }
 
   public function getHTTPResponseMessage() {
     switch ($this->getHTTPResponseCode()) {
       case 100: return 'Continue';
       case 101: return 'Switching Protocols';
       case 200: return 'OK';
       case 201: return 'Created';
       case 202: return 'Accepted';
       case 203: return 'Non-Authoritative Information';
       case 204: return 'No Content';
       case 205: return 'Reset Content';
       case 206: return 'Partial Content';
       case 300: return 'Multiple Choices';
       case 301: return 'Moved Permanently';
       case 302: return 'Found';
       case 303: return 'See Other';
       case 304: return 'Not Modified';
       case 305: return 'Use Proxy';
       case 306: return 'Switch Proxy';
       case 307: return 'Temporary Redirect';
       case 400: return 'Bad Request';
       case 401: return 'Unauthorized';
       case 402: return 'Payment Required';
       case 403: return 'Forbidden';
       case 404: return 'Not Found';
       case 405: return 'Method Not Allowed';
       case 406: return 'Not Acceptable';
       case 407: return 'Proxy Authentication Required';
       case 408: return 'Request Timeout';
       case 409: return 'Conflict';
       case 410: return 'Gone';
       case 411: return 'Length Required';
       case 412: return 'Precondition Failed';
       case 413: return 'Request Entity Too Large';
       case 414: return 'Request-URI Too Long';
       case 415: return 'Unsupported Media Type';
       case 416: return 'Requested Range Not Satisfiable';
       case 417: return 'Expectation Failed';
       case 418: return "I'm a teapot";
       case 426: return 'Upgrade Required';
       case 500: return 'Internal Server Error';
       case 501: return 'Not Implemented';
       case 502: return 'Bad Gateway';
       case 503: return 'Service Unavailable';
       case 504: return 'Gateway Timeout';
       case 505: return 'HTTP Version Not Supported';
       default:  return '';
     }
   }
 
   public function setFrameable($frameable) {
     $this->frameable = $frameable;
     return $this;
   }
 
   public static function processValueForJSONEncoding(&$value, $key) {
     if ($value instanceof PhutilSafeHTMLProducerInterface) {
       // This renders the producer down to PhutilSafeHTML, which will then
       // be simplified into a string below.
       $value = hsprintf('%s', $value);
     }
 
     if ($value instanceof PhutilSafeHTML) {
       // TODO: Javelin supports implicity conversion of '__html' objects to
       // JX.HTML, but only for Ajax responses, not behaviors. Just leave things
       // as they are for now (where behaviors treat responses as HTML or plain
       // text at their discretion).
       $value = $value->getHTMLContent();
     }
   }
 
   public static function encodeJSONForHTTPResponse(array $object) {
 
     array_walk_recursive(
       $object,
       array(__CLASS__, 'processValueForJSONEncoding'));
 
     $response = json_encode($object);
 
     // Prevent content sniffing attacks by encoding "<" and ">", so browsers
     // won't try to execute the document as HTML even if they ignore
     // Content-Type and X-Content-Type-Options. See T865.
     $response = str_replace(
       array('<', '>'),
       array('\u003c', '\u003e'),
       $response);
 
     return $response;
   }
 
   protected function addJSONShield($json_response) {
     // Add a shield to prevent "JSON Hijacking" attacks where an attacker
     // requests a JSON response using a normal <script /> tag and then uses
     // Object.prototype.__defineSetter__() or similar to read response data.
     // This header causes the browser to loop infinitely instead of handing over
     // sensitive data.
 
     $shield = 'for (;;);';
 
     $response = $shield.$json_response;
 
     return $response;
   }
 
   public function getCacheHeaders() {
     $headers = array();
     if ($this->cacheable) {
       $headers[] = array(
         'Expires',
         $this->formatEpochTimestampForHTTPHeader(time() + $this->cacheable),
       );
     } else {
       $headers[] = array(
         'Cache-Control',
         'private, no-cache, no-store, must-revalidate',
       );
       $headers[] = array(
         'Pragma',
         'no-cache',
       );
       $headers[] = array(
         'Expires',
         'Sat, 01 Jan 2000 00:00:00 GMT',
       );
     }
 
     if ($this->lastModified) {
       $headers[] = array(
         'Last-Modified',
         $this->formatEpochTimestampForHTTPHeader($this->lastModified),
       );
     }
 
     // IE has a feature where it may override an explicit Content-Type
     // declaration by inferring a content type. This can be a security risk
     // and we always explicitly transmit the correct Content-Type header, so
     // prevent IE from using inferred content types. This only offers protection
     // on recent versions of IE; IE6/7 and Opera currently ignore this header.
     $headers[] = array('X-Content-Type-Options', 'nosniff');
 
     return $headers;
   }
 
   private function formatEpochTimestampForHTTPHeader($epoch_timestamp) {
     return gmdate('D, d M Y H:i:s', $epoch_timestamp).' GMT';
   }
 
   public function didCompleteWrite($aborted) {
     return;
   }
 
 }
diff --git a/src/aphront/sink/AphrontHTTPSink.php b/src/aphront/sink/AphrontHTTPSink.php
index 6ed2edff9..4a729da16 100644
--- a/src/aphront/sink/AphrontHTTPSink.php
+++ b/src/aphront/sink/AphrontHTTPSink.php
@@ -1,134 +1,134 @@
 <?php
 
 /**
  * Abstract class which wraps some sort of output mechanism for HTTP responses.
  * Normally this is just @{class:AphrontPHPHTTPSink}, which uses "echo" and
  * "header()" to emit responses.
  *
  * Mostly, this class allows us to do install security or metrics hooks in the
  * output pipeline.
  *
  * @task write  Writing Response Components
  * @task emit   Emitting the Response
  */
-abstract class AphrontHTTPSink {
+abstract class AphrontHTTPSink extends Phobject {
 
 
 /* -(  Writing Response Components  )---------------------------------------- */
 
 
   /**
    * Write an HTTP status code to the output.
    *
    * @param int Numeric HTTP status code.
    * @return void
    */
   final public function writeHTTPStatus($code, $message = '') {
     if (!preg_match('/^\d{3}$/', $code)) {
       throw new Exception(pht("Malformed HTTP status code '%s'!", $code));
     }
 
     $code = (int)$code;
     $this->emitHTTPStatus($code, $message);
   }
 
 
   /**
    * Write HTTP headers to the output.
    *
    * @param list<pair> List of <name, value> pairs.
    * @return void
    */
   final public function writeHeaders(array $headers) {
     foreach ($headers as $header) {
       if (!is_array($header) || count($header) !== 2) {
         throw new Exception(pht('Malformed header.'));
       }
       list($name, $value) = $header;
 
       if (strpos($name, ':') !== false) {
         throw new Exception(
           pht(
             'Declining to emit response with malformed HTTP header name: %s',
             $name));
       }
 
       // Attackers may perform an "HTTP response splitting" attack by making
       // the application emit certain types of headers containing newlines:
       //
       //   http://en.wikipedia.org/wiki/HTTP_response_splitting
       //
       // PHP has built-in protections against HTTP response-splitting, but they
       // are of dubious trustworthiness:
       //
       //   http://news.php.net/php.internals/57655
 
       if (preg_match('/[\r\n\0]/', $name.$value)) {
         throw new Exception(
           pht(
             'Declining to emit response with unsafe HTTP header: %s',
             "<'".$name."', '".$value."'>."));
       }
     }
 
     foreach ($headers as $header) {
       list($name, $value) = $header;
       $this->emitHeader($name, $value);
     }
   }
 
 
   /**
    * Write HTTP body data to the output.
    *
    * @param string Body data.
    * @return void
    */
   final public function writeData($data) {
     $this->emitData($data);
   }
 
 
   /**
    * Write an entire @{class:AphrontResponse} to the output.
    *
    * @param AphrontResponse The response object to write.
    * @return void
    */
   final public function writeResponse(AphrontResponse $response) {
     // Build the content iterator first, in case it throws. Ideally, we'd
     // prefer to handle exceptions before we emit the response status or any
     // HTTP headers.
     $data = $response->getContentIterator();
 
     $all_headers = array_merge(
       $response->getHeaders(),
       $response->getCacheHeaders());
 
     $this->writeHTTPStatus(
       $response->getHTTPResponseCode(),
       $response->getHTTPResponseMessage());
     $this->writeHeaders($all_headers);
 
     $abort = false;
     foreach ($data as $block) {
       if (!$this->isWritable()) {
         $abort = true;
         break;
       }
       $this->writeData($block);
     }
 
     $response->didCompleteWrite($abort);
   }
 
 
 /* -(  Emitting the Response  )---------------------------------------------- */
 
 
   abstract protected function emitHTTPStatus($code, $message = '');
   abstract protected function emitHeader($name, $value);
   abstract protected function emitData($data);
   abstract protected function isWritable();
 
 }
diff --git a/src/applications/almanac/servicetype/__tests__/AlmanacServiceTypeTestCase.php b/src/applications/almanac/servicetype/__tests__/AlmanacServiceTypeTestCase.php
new file mode 100644
index 000000000..535c07f6d
--- /dev/null
+++ b/src/applications/almanac/servicetype/__tests__/AlmanacServiceTypeTestCase.php
@@ -0,0 +1,10 @@
+<?php
+
+final class AlmanacServiceTypeTestCase extends PhabricatorTestCase {
+
+  public function testGetAllServiceTypes() {
+    AlmanacServiceType::getAllServiceTypes();
+    $this->assertTrue(true);
+  }
+
+}
diff --git a/src/applications/aphlict/query/AphlictDropdownDataQuery.php b/src/applications/aphlict/query/AphlictDropdownDataQuery.php
index 4f360f28c..e8a8edb59 100644
--- a/src/applications/aphlict/query/AphlictDropdownDataQuery.php
+++ b/src/applications/aphlict/query/AphlictDropdownDataQuery.php
@@ -1,103 +1,103 @@
 <?php
 
-final class AphlictDropdownDataQuery {
+final class AphlictDropdownDataQuery extends Phobject {
 
   private $viewer;
   private $notificationData;
   private $conpherenceData;
 
   public function setViewer(PhabricatorUser $viewer) {
     $this->viewer = $viewer;
     return $this;
   }
 
   public function getViewer() {
     return $this->viewer;
   }
 
   private function setNotificationData(array $data) {
     $this->notificationData = $data;
     return $this;
   }
 
   public function getNotificationData() {
     if ($this->notificationData === null) {
       throw new Exception(pht('You must %s first!', 'execute()'));
     }
     return $this->notificationData;
   }
 
   private function setConpherenceData(array $data) {
     $this->conpherenceData = $data;
     return $this;
   }
 
   public function getConpherenceData() {
     if ($this->conpherenceData === null) {
       throw new Exception(pht('You must %s first!', 'execute()'));
     }
     return $this->conpherenceData;
   }
 
   public function execute() {
     $viewer = $this->getViewer();
 
     $conpherence_app = 'PhabricatorConpherenceApplication';
     $is_c_installed = PhabricatorApplication::isClassInstalledForViewer(
       $conpherence_app,
       $viewer);
     $raw_message_count_number = null;
     $message_count_number = null;
     if ($is_c_installed) {
       $unread_status = ConpherenceParticipationStatus::BEHIND;
       $unread = id(new ConpherenceParticipantCountQuery())
         ->withParticipantPHIDs(array($viewer->getPHID()))
         ->withParticipationStatus($unread_status)
         ->execute();
       $raw_message_count_number = idx($unread, $viewer->getPHID(), 0);
       $message_count_number = $this->formatNumber($raw_message_count_number);
     }
     $conpherence_data = array(
       'isInstalled' => $is_c_installed,
       'countType' => 'messages',
       'count' => $message_count_number,
       'rawCount' => $raw_message_count_number,
     );
     $this->setConpherenceData($conpherence_data);
 
     $notification_app = 'PhabricatorNotificationsApplication';
     $is_n_installed = PhabricatorApplication::isClassInstalledForViewer(
       $notification_app,
       $viewer);
     $notification_count_number = null;
     $raw_notification_count_number = null;
     if ($is_n_installed) {
       $raw_notification_count_number =
         id(new PhabricatorFeedStoryNotification())
         ->countUnread($viewer);
       $notification_count_number = $this->formatNumber(
         $raw_notification_count_number);
     }
     $notification_data = array(
       'isInstalled' => $is_n_installed,
       'countType' => 'notifications',
       'count' => $notification_count_number,
       'rawCount' => $raw_notification_count_number,
     );
     $this->setNotificationData($notification_data);
 
     return array(
       $notification_app => $this->getNotificationData(),
       $conpherence_app => $this->getConpherenceData(),
     );
   }
 
   private function formatNumber($number) {
     $formatted = $number;
     if ($number > 999) {
       $formatted = "\xE2\x88\x9E";
     }
     return $formatted;
   }
 
 }
diff --git a/src/applications/audit/constants/PhabricatorAuditActionConstants.php b/src/applications/audit/constants/PhabricatorAuditActionConstants.php
index afecd0bf8..ca2a497af 100644
--- a/src/applications/audit/constants/PhabricatorAuditActionConstants.php
+++ b/src/applications/audit/constants/PhabricatorAuditActionConstants.php
@@ -1,47 +1,47 @@
 <?php
 
-final class PhabricatorAuditActionConstants {
+final class PhabricatorAuditActionConstants extends Phobject {
 
   const CONCERN   = 'concern';
   const ACCEPT    = 'accept';
   const COMMENT   = 'comment';
   const RESIGN    = 'resign';
   const CLOSE     = 'close';
   const ADD_CCS = 'add_ccs';
   const ADD_AUDITORS = 'add_auditors';
   const INLINE = 'audit:inline';
   const ACTION = 'audit:action';
 
   public static function getActionNameMap() {
     $map = array(
       self::COMMENT      => pht('Comment'),
       self::CONCERN      => pht("Raise Concern \xE2\x9C\x98"),
       self::ACCEPT       => pht("Accept Commit \xE2\x9C\x94"),
       self::RESIGN       => pht('Resign from Audit'),
       self::CLOSE        => pht('Close Audit'),
       self::ADD_CCS      => pht('Add CCs'),
       self::ADD_AUDITORS => pht('Add Auditors'),
     );
 
     return $map;
   }
 
   public static function getActionName($constant) {
     $map = self::getActionNameMap();
     return idx($map, $constant, pht('Unknown'));
   }
 
   public static function getActionPastTenseVerb($action) {
     $map = array(
       self::COMMENT      => pht('commented on'),
       self::CONCERN      => pht('raised a concern with'),
       self::ACCEPT       => pht('accepted'),
       self::RESIGN       => pht('resigned from'),
       self::CLOSE        => pht('closed'),
       self::ADD_CCS      => pht('added CCs to'),
       self::ADD_AUDITORS => pht('added auditors to'),
     );
     return idx($map, $action, pht('updated'));
   }
 
 }
diff --git a/src/applications/audit/constants/PhabricatorAuditCommitStatusConstants.php b/src/applications/audit/constants/PhabricatorAuditCommitStatusConstants.php
index 1bc07aaa5..52fa110c3 100644
--- a/src/applications/audit/constants/PhabricatorAuditCommitStatusConstants.php
+++ b/src/applications/audit/constants/PhabricatorAuditCommitStatusConstants.php
@@ -1,53 +1,53 @@
 <?php
 
-final class PhabricatorAuditCommitStatusConstants {
+final class PhabricatorAuditCommitStatusConstants extends Phobject {
 
   const NONE                = 0;
   const NEEDS_AUDIT         = 1;
   const CONCERN_RAISED      = 2;
   const PARTIALLY_AUDITED   = 3;
   const FULLY_AUDITED       = 4;
 
   public static function getStatusNameMap() {
     $map = array(
       self::NONE                => pht('None'),
       self::NEEDS_AUDIT         => pht('Audit Required'),
       self::CONCERN_RAISED      => pht('Concern Raised'),
       self::PARTIALLY_AUDITED   => pht('Partially Audited'),
       self::FULLY_AUDITED       => pht('Audited'),
     );
 
     return $map;
   }
 
   public static function getStatusName($code) {
     return idx(self::getStatusNameMap(), $code, pht('Unknown'));
   }
 
   public static function getOpenStatusConstants() {
     return array(
       self::CONCERN_RAISED,
       self::NEEDS_AUDIT,
     );
   }
 
   public static function getStatusColor($code) {
     switch ($code) {
       case self::CONCERN_RAISED:
         $color = 'red';
         break;
       case self::NEEDS_AUDIT:
       case self::PARTIALLY_AUDITED:
         $color = 'orange';
         break;
       case self::FULLY_AUDITED:
         $color = 'green';
         break;
       default:
         $color = null;
         break;
     }
     return $color;
   }
 
 }
diff --git a/src/applications/audit/constants/PhabricatorAuditStatusConstants.php b/src/applications/audit/constants/PhabricatorAuditStatusConstants.php
index 78924905b..5920b7f35 100644
--- a/src/applications/audit/constants/PhabricatorAuditStatusConstants.php
+++ b/src/applications/audit/constants/PhabricatorAuditStatusConstants.php
@@ -1,97 +1,97 @@
 <?php
 
-final class PhabricatorAuditStatusConstants {
+final class PhabricatorAuditStatusConstants extends Phobject {
 
   const NONE                    = '';
   const AUDIT_NOT_REQUIRED      = 'audit-not-required';
   const AUDIT_REQUIRED          = 'audit-required';
   const CONCERNED               = 'concerned';
   const ACCEPTED                = 'accepted';
   const AUDIT_REQUESTED         = 'requested';
   const RESIGNED                = 'resigned';
   const CLOSED                  = 'closed';
   const CC                      = 'cc';
 
   public static function getStatusNameMap() {
     $map = array(
       self::NONE                => pht('Not Applicable'),
       self::AUDIT_NOT_REQUIRED  => pht('Audit Not Required'),
       self::AUDIT_REQUIRED      => pht('Audit Required'),
       self::CONCERNED           => pht('Concern Raised'),
       self::ACCEPTED            => pht('Accepted'),
       self::AUDIT_REQUESTED     => pht('Audit Requested'),
       self::RESIGNED            => pht('Resigned'),
       self::CLOSED              => pht('Closed'),
       self::CC                  => pht("Was CC'd"),
     );
 
     return $map;
   }
 
   public static function getStatusName($code) {
     return idx(self::getStatusNameMap(), $code, pht('Unknown'));
   }
 
   public static function getStatusColor($code) {
     switch ($code) {
       case self::CONCERNED:
         $color = 'red';
         break;
       case self::AUDIT_REQUIRED:
       case self::AUDIT_REQUESTED:
         $color = 'orange';
         break;
       case self::ACCEPTED:
         $color = 'green';
         break;
       case self::AUDIT_NOT_REQUIRED:
         $color = 'blue';
         break;
       case self::RESIGNED:
       case self::CLOSED:
         $color = 'dark';
         break;
       default:
         $color = 'bluegrey';
         break;
     }
     return $color;
   }
 
   public static function getStatusIcon($code) {
     switch ($code) {
       case self::AUDIT_NOT_REQUIRED:
       case self::RESIGNED:
         $icon = PHUIStatusItemView::ICON_OPEN;
         break;
       case self::AUDIT_REQUIRED:
       case self::AUDIT_REQUESTED:
         $icon = PHUIStatusItemView::ICON_WARNING;
         break;
       case self::CONCERNED:
         $icon = PHUIStatusItemView::ICON_REJECT;
         break;
       case self::ACCEPTED:
       case self::CLOSED:
         $icon = PHUIStatusItemView::ICON_ACCEPT;
         break;
       default:
         $icon = PHUIStatusItemView::ICON_QUESTION;
         break;
     }
     return $icon;
   }
 
   public static function getOpenStatusConstants() {
     return array(
       self::AUDIT_REQUIRED,
       self::AUDIT_REQUESTED,
       self::CONCERNED,
     );
   }
 
   public static function isOpenStatus($status) {
     return in_array($status, self::getOpenStatusConstants());
   }
 
 }
diff --git a/src/applications/audit/storage/PhabricatorAuditInlineComment.php b/src/applications/audit/storage/PhabricatorAuditInlineComment.php
index 8779b5ffd..fc28bce72 100644
--- a/src/applications/audit/storage/PhabricatorAuditInlineComment.php
+++ b/src/applications/audit/storage/PhabricatorAuditInlineComment.php
@@ -1,354 +1,355 @@
 <?php
 
 final class PhabricatorAuditInlineComment
+  extends Phobject
   implements PhabricatorInlineCommentInterface {
 
   private $proxy;
   private $syntheticAuthor;
   private $isGhost;
 
   public function __construct() {
     $this->proxy = new PhabricatorAuditTransactionComment();
   }
 
   public function __clone() {
     $this->proxy = clone $this->proxy;
   }
 
   public function getTransactionPHID() {
     return $this->proxy->getTransactionPHID();
   }
 
   public function getTransactionComment() {
     return $this->proxy;
   }
 
   public function supportsHiding() {
     return false;
   }
 
   public function isHidden() {
     return false;
   }
 
   public function getTransactionCommentForSave() {
     $content_source = PhabricatorContentSource::newForSource(
       PhabricatorContentSource::SOURCE_LEGACY,
       array());
 
     $this->proxy
       ->setViewPolicy('public')
       ->setEditPolicy($this->getAuthorPHID())
       ->setContentSource($content_source)
       ->setCommentVersion(1);
 
     return $this->proxy;
   }
 
   public static function loadID($id) {
     $inlines = id(new PhabricatorAuditTransactionComment())->loadAllWhere(
       'id = %d',
       $id);
     if (!$inlines) {
       return null;
     }
 
     return head(self::buildProxies($inlines));
   }
 
   public static function loadPHID($phid) {
     $inlines = id(new PhabricatorAuditTransactionComment())->loadAllWhere(
       'phid = %s',
       $phid);
     if (!$inlines) {
       return null;
     }
     return head(self::buildProxies($inlines));
   }
 
   public static function loadDraftComments(
     PhabricatorUser $viewer,
     $commit_phid) {
 
     $inlines = id(new DiffusionDiffInlineCommentQuery())
       ->setViewer($viewer)
       ->withAuthorPHIDs(array($viewer->getPHID()))
       ->withCommitPHIDs(array($commit_phid))
       ->withHasTransaction(false)
       ->withHasPath(true)
       ->withIsDeleted(false)
       ->needReplyToComments(true)
       ->execute();
 
     return self::buildProxies($inlines);
   }
 
   public static function loadPublishedComments(
     PhabricatorUser $viewer,
     $commit_phid) {
 
     $inlines = id(new DiffusionDiffInlineCommentQuery())
       ->setViewer($viewer)
       ->withCommitPHIDs(array($commit_phid))
       ->withHasTransaction(true)
       ->withHasPath(true)
       ->execute();
 
     return self::buildProxies($inlines);
   }
 
   public static function loadDraftAndPublishedComments(
     PhabricatorUser $viewer,
     $commit_phid,
     $path_id = null) {
 
     if ($path_id === null) {
       $inlines = id(new PhabricatorAuditTransactionComment())->loadAllWhere(
         'commitPHID = %s AND (transactionPHID IS NOT NULL OR authorPHID = %s)
           AND pathID IS NOT NULL',
         $commit_phid,
         $viewer->getPHID());
     } else {
       $inlines = id(new PhabricatorAuditTransactionComment())->loadAllWhere(
         'commitPHID = %s AND pathID = %d AND
           ((authorPHID = %s AND isDeleted = 0) OR transactionPHID IS NOT NULL)',
         $commit_phid,
         $path_id,
         $viewer->getPHID());
     }
 
     return self::buildProxies($inlines);
   }
 
   private static function buildProxies(array $inlines) {
     $results = array();
     foreach ($inlines as $key => $inline) {
       $results[$key] = self::newFromModernComment(
         $inline);
     }
     return $results;
   }
 
   public function setSyntheticAuthor($synthetic_author) {
     $this->syntheticAuthor = $synthetic_author;
     return $this;
   }
 
   public function getSyntheticAuthor() {
     return $this->syntheticAuthor;
   }
 
   public function openTransaction() {
     $this->proxy->openTransaction();
   }
 
   public function saveTransaction() {
     $this->proxy->saveTransaction();
   }
 
   public function save() {
     $this->getTransactionCommentForSave()->save();
 
     return $this;
   }
 
   public function delete() {
     $this->proxy->delete();
 
     return $this;
   }
 
   public function getID() {
     return $this->proxy->getID();
   }
 
   public function getPHID() {
     return $this->proxy->getPHID();
   }
 
   public static function newFromModernComment(
     PhabricatorAuditTransactionComment $comment) {
 
     $obj = new PhabricatorAuditInlineComment();
     $obj->proxy = $comment;
 
     return $obj;
   }
 
   public function isCompatible(PhabricatorInlineCommentInterface $comment) {
     return
       ($this->getAuthorPHID() === $comment->getAuthorPHID()) &&
       ($this->getSyntheticAuthor() === $comment->getSyntheticAuthor()) &&
       ($this->getContent() === $comment->getContent());
   }
 
   public function setContent($content) {
     $this->proxy->setContent($content);
     return $this;
   }
 
   public function getContent() {
     return $this->proxy->getContent();
   }
 
   public function isDraft() {
     return !$this->proxy->getTransactionPHID();
   }
 
   public function setPathID($id) {
     $this->proxy->setPathID($id);
     return $this;
   }
 
   public function getPathID() {
     return $this->proxy->getPathID();
   }
 
   public function setIsNewFile($is_new) {
     $this->proxy->setIsNewFile($is_new);
     return $this;
   }
 
   public function getIsNewFile() {
     return $this->proxy->getIsNewFile();
   }
 
   public function setLineNumber($number) {
     $this->proxy->setLineNumber($number);
     return $this;
   }
 
   public function getLineNumber() {
     return $this->proxy->getLineNumber();
   }
 
   public function setLineLength($length) {
     $this->proxy->setLineLength($length);
     return $this;
   }
 
   public function getLineLength() {
     return $this->proxy->getLineLength();
   }
 
   public function setCache($cache) {
     return $this;
   }
 
   public function getCache() {
     return null;
   }
 
   public function setAuthorPHID($phid) {
     $this->proxy->setAuthorPHID($phid);
     return $this;
   }
 
   public function getAuthorPHID() {
     return $this->proxy->getAuthorPHID();
   }
 
   public function setCommitPHID($commit_phid) {
     $this->proxy->setCommitPHID($commit_phid);
     return $this;
   }
 
   public function getCommitPHID() {
     return $this->proxy->getCommitPHID();
   }
 
   // When setting a comment ID, we also generate a phantom transaction PHID for
   // the future transaction.
 
   public function setAuditCommentID($id) {
     $this->proxy->setLegacyCommentID($id);
     $this->proxy->setTransactionPHID(
       PhabricatorPHID::generateNewPHID(
         PhabricatorApplicationTransactionTransactionPHIDType::TYPECONST,
         PhabricatorRepositoryCommitPHIDType::TYPECONST));
     return $this;
   }
 
   public function getAuditCommentID() {
     return $this->proxy->getLegacyCommentID();
   }
 
   public function setChangesetID($id) {
     return $this->setPathID($id);
   }
 
   public function getChangesetID() {
     return $this->getPathID();
   }
 
   public function setReplyToCommentPHID($phid) {
     $this->proxy->setReplyToCommentPHID($phid);
     return $this;
   }
 
   public function getReplyToCommentPHID() {
     return $this->proxy->getReplyToCommentPHID();
   }
 
   public function setHasReplies($has_replies) {
     $this->proxy->setHasReplies($has_replies);
     return $this;
   }
 
   public function getHasReplies() {
     return $this->proxy->getHasReplies();
   }
 
   public function setIsDeleted($is_deleted) {
     $this->proxy->setIsDeleted($is_deleted);
     return $this;
   }
 
   public function getIsDeleted() {
     return $this->proxy->getIsDeleted();
   }
 
   public function setFixedState($state) {
     $this->proxy->setFixedState($state);
     return $this;
   }
 
   public function getFixedState() {
     return $this->proxy->getFixedState();
   }
 
   public function setIsGhost($is_ghost) {
     $this->isGhost = $is_ghost;
     return $this;
   }
 
   public function getIsGhost() {
     return $this->isGhost;
   }
 
 
 /* -(  PhabricatorMarkupInterface Implementation  )-------------------------- */
 
 
   public function getMarkupFieldKey($field) {
     return 'AI:'.$this->getID();
   }
 
   public function newMarkupEngine($field) {
     return PhabricatorMarkupEngine::newDifferentialMarkupEngine();
   }
 
   public function getMarkupText($field) {
     return $this->getContent();
   }
 
   public function didMarkupText($field, $output, PhutilMarkupEngine $engine) {
     return $output;
   }
 
   public function shouldUseMarkupCache($field) {
     // Only cache submitted comments.
     return ($this->getID() && $this->getAuditCommentID());
   }
 
 }
diff --git a/src/applications/auth/controller/PhabricatorAuthStartController.php b/src/applications/auth/controller/PhabricatorAuthStartController.php
index f9b9f7cb4..01578446a 100644
--- a/src/applications/auth/controller/PhabricatorAuthStartController.php
+++ b/src/applications/auth/controller/PhabricatorAuthStartController.php
@@ -1,243 +1,243 @@
 <?php
 
 final class PhabricatorAuthStartController
   extends PhabricatorAuthController {
 
   public function shouldRequireLogin() {
     return false;
   }
 
   public function handleRequest(AphrontRequest $request) {
     $viewer = $request->getUser();
 
     if ($viewer->isLoggedIn()) {
       // Kick the user home if they are already logged in.
       return id(new AphrontRedirectResponse())->setURI('/');
     }
 
     if ($request->isAjax()) {
       return $this->processAjaxRequest();
     }
 
     if ($request->isConduit()) {
       return $this->processConduitRequest();
     }
 
     // If the user gets this far, they aren't logged in, so if they have a
     // user session token we can conclude that it's invalid: if it was valid,
     // they'd have been logged in above and never made it here. Try to clear
     // it and warn the user they may need to nuke their cookies.
 
     $session_token = $request->getCookie(PhabricatorCookies::COOKIE_SESSION);
 
     if (strlen($session_token)) {
       $kind = PhabricatorAuthSessionEngine::getSessionKindFromToken(
         $session_token);
       switch ($kind) {
         case PhabricatorAuthSessionEngine::KIND_ANONYMOUS:
           // If this is an anonymous session. It's expected that they won't
           // be logged in, so we can just continue.
           break;
         default:
           // The session cookie is invalid, so clear it.
           $request->clearCookie(PhabricatorCookies::COOKIE_USERNAME);
           $request->clearCookie(PhabricatorCookies::COOKIE_SESSION);
 
           return $this->renderError(
             pht(
               'Your login session is invalid. Try reloading the page and '.
               'logging in again. If that does not work, clear your browser '.
               'cookies.'));
       }
     }
 
     $providers = PhabricatorAuthProvider::getAllEnabledProviders();
     foreach ($providers as $key => $provider) {
       if (!$provider->shouldAllowLogin()) {
         unset($providers[$key]);
       }
     }
 
     if (!$providers) {
       if ($this->isFirstTimeSetup()) {
         // If this is a fresh install, let the user register their admin
         // account.
         return id(new AphrontRedirectResponse())
           ->setURI($this->getApplicationURI('/register/'));
       }
 
       return $this->renderError(
         pht(
           'This Phabricator install is not configured with any enabled '.
           'authentication providers which can be used to log in. If you '.
           'have accidentally locked yourself out by disabling all providers, '.
-          'you can use `%s` to recover access to an administrative account.'.
+          'you can use `%s` to recover access to an administrative account.',
           'phabricator/bin/auth recover <username>'));
     }
 
     $next_uri = $request->getStr('next');
     if (!strlen($next_uri)) {
       if ($this->getDelegatingController()) {
         // Only set a next URI from the request path if this controller was
         // delegated to, which happens when a user tries to view a page which
         // requires them to login.
 
         // If this controller handled the request directly, we're on the main
         // login page, and never want to redirect the user back here after they
         // login.
         $next_uri = (string)$this->getRequest()->getRequestURI();
       }
     }
 
     if (!$request->isFormPost()) {
       if (strlen($next_uri)) {
         PhabricatorCookies::setNextURICookie($request, $next_uri);
       }
       PhabricatorCookies::setClientIDCookie($request);
     }
 
     if (!$request->getURIData('loggedout') && count($providers) == 1) {
       $auto_login_provider = head($providers);
       $auto_login_config = $auto_login_provider->getProviderConfig();
       if ($auto_login_provider instanceof PhabricatorPhabricatorAuthProvider &&
           $auto_login_config->getShouldAutoLogin()) {
         $auto_login_adapter = $provider->getAdapter();
         $auto_login_adapter->setState($provider->getAuthCSRFCode($request));
         return id(new AphrontRedirectResponse())
           ->setIsExternal(true)
           ->setURI($provider->getAdapter()->getAuthenticateURI());
       }
     }
 
     $invite = $this->loadInvite();
 
     $not_buttons = array();
     $are_buttons = array();
     $providers = msort($providers, 'getLoginOrder');
     foreach ($providers as $provider) {
       if ($invite) {
         $form = $provider->buildInviteForm($this);
       } else {
         $form = $provider->buildLoginForm($this);
       }
       if ($provider->isLoginFormAButton()) {
         $are_buttons[] = $form;
       } else {
         $not_buttons[] = $form;
       }
     }
 
     $out = array();
     $out[] = $not_buttons;
     if ($are_buttons) {
       require_celerity_resource('auth-css');
 
       foreach ($are_buttons as $key => $button) {
         $are_buttons[$key] = phutil_tag(
           'div',
           array(
             'class' => 'phabricator-login-button mmb',
           ),
           $button);
       }
 
       // If we only have one button, add a second pretend button so that we
       // always have two columns. This makes it easier to get the alignments
       // looking reasonable.
       if (count($are_buttons) == 1) {
         $are_buttons[] = null;
       }
 
       $button_columns = id(new AphrontMultiColumnView())
         ->setFluidLayout(true);
       $are_buttons = array_chunk($are_buttons, ceil(count($are_buttons) / 2));
       foreach ($are_buttons as $column) {
         $button_columns->addColumn($column);
       }
 
       $out[] = phutil_tag(
         'div',
         array(
           'class' => 'phabricator-login-buttons',
         ),
         $button_columns);
     }
 
     $login_message = PhabricatorEnv::getEnvConfig('auth.login-message');
     $login_message = phutil_safe_html($login_message);
 
     $invite_message = null;
     if ($invite) {
       $invite_message = $this->renderInviteHeader($invite);
     }
 
     $crumbs = $this->buildApplicationCrumbs();
     $crumbs->addTextCrumb(pht('Login'));
     $crumbs->setBorder(true);
 
     return $this->buildApplicationPage(
       array(
         $crumbs,
         $login_message,
         $invite_message,
         $out,
       ),
       array(
         'title' => pht('Login to Phabricator'),
       ));
   }
 
 
   private function processAjaxRequest() {
     $request = $this->getRequest();
     $viewer = $request->getUser();
 
     // We end up here if the user clicks a workflow link that they need to
     // login to use. We give them a dialog saying "You need to login...".
 
     if ($request->isDialogFormPost()) {
       return id(new AphrontRedirectResponse())->setURI(
         $request->getRequestURI());
     }
 
     $dialog = new AphrontDialogView();
     $dialog->setUser($viewer);
     $dialog->setTitle(pht('Login Required'));
     $dialog->appendChild(pht('You must login to continue.'));
     $dialog->addSubmitButton(pht('Login'));
     $dialog->addCancelButton('/');
 
     return id(new AphrontDialogResponse())->setDialog($dialog);
   }
 
 
   private function processConduitRequest() {
     $request = $this->getRequest();
     $viewer = $request->getUser();
 
     // A common source of errors in Conduit client configuration is getting
     // the request path wrong. The client will end up here, so make some
     // effort to give them a comprehensible error message.
 
     $request_path = $this->getRequest()->getPath();
     $conduit_path = '/api/<method>';
     $example_path = '/api/conduit.ping';
 
     $message = pht(
       'ERROR: You are making a Conduit API request to "%s", but the correct '.
       'HTTP request path to use in order to access a COnduit method is "%s" '.
       '(for example, "%s"). Check your configuration.',
       $request_path,
       $conduit_path,
       $example_path);
 
     return id(new AphrontPlainTextResponse())->setContent($message);
   }
 
   protected function renderError($message) {
     return $this->renderErrorPage(
       pht('Authentication Failure'),
       array($message));
   }
 
 }
diff --git a/src/applications/auth/data/PhabricatorAuthHighSecurityToken.php b/src/applications/auth/data/PhabricatorAuthHighSecurityToken.php
index 5c84cc9fd..8ea1ed97f 100644
--- a/src/applications/auth/data/PhabricatorAuthHighSecurityToken.php
+++ b/src/applications/auth/data/PhabricatorAuthHighSecurityToken.php
@@ -1,3 +1,3 @@
 <?php
 
-final class PhabricatorAuthHighSecurityToken {}
+final class PhabricatorAuthHighSecurityToken extends Phobject {}
diff --git a/src/applications/auth/factor/__tests__/PhabricatorAuthFactorTestCase.php b/src/applications/auth/factor/__tests__/PhabricatorAuthFactorTestCase.php
new file mode 100644
index 000000000..ccc4fc5c9
--- /dev/null
+++ b/src/applications/auth/factor/__tests__/PhabricatorAuthFactorTestCase.php
@@ -0,0 +1,10 @@
+<?php
+
+final class PhabricatorAuthFactorTestCase extends PhabricatorTestCase {
+
+  public function testGetAllFactors() {
+    PhabricatorAuthFactor::getAllFactors();
+    $this->assertTrue(true);
+  }
+
+}
diff --git a/src/applications/auth/provider/PhabricatorAuthProvider.php b/src/applications/auth/provider/PhabricatorAuthProvider.php
index d495a3091..0b03bed7f 100644
--- a/src/applications/auth/provider/PhabricatorAuthProvider.php
+++ b/src/applications/auth/provider/PhabricatorAuthProvider.php
@@ -1,501 +1,501 @@
 <?php
 
-abstract class PhabricatorAuthProvider {
+abstract class PhabricatorAuthProvider extends Phobject {
 
   private $providerConfig;
 
   public function attachProviderConfig(PhabricatorAuthProviderConfig $config) {
     $this->providerConfig = $config;
     return $this;
   }
 
   public function hasProviderConfig() {
     return (bool)$this->providerConfig;
   }
 
   public function getProviderConfig() {
     if ($this->providerConfig === null) {
       throw new PhutilInvalidStateException('attachProviderConfig');
     }
     return $this->providerConfig;
   }
 
   public function getConfigurationHelp() {
     return null;
   }
 
   public function getDefaultProviderConfig() {
     return id(new PhabricatorAuthProviderConfig())
       ->setProviderClass(get_class($this))
       ->setIsEnabled(1)
       ->setShouldAllowLogin(1)
       ->setShouldAllowRegistration(1)
       ->setShouldAllowLink(1)
       ->setShouldAllowUnlink(1);
   }
 
   public function getNameForCreate() {
     return $this->getProviderName();
   }
 
   public function getDescriptionForCreate() {
     return null;
   }
 
   public function getProviderKey() {
     return $this->getAdapter()->getAdapterKey();
   }
 
   public function getProviderType() {
     return $this->getAdapter()->getAdapterType();
   }
 
   public function getProviderDomain() {
     return $this->getAdapter()->getAdapterDomain();
   }
 
   public static function getAllBaseProviders() {
     static $providers;
 
     if ($providers === null) {
       $objects = id(new PhutilSymbolLoader())
         ->setAncestorClass(__CLASS__)
         ->loadObjects();
       $providers = $objects;
     }
 
     return $providers;
   }
 
   public static function getAllProviders() {
     static $providers;
 
     if ($providers === null) {
       $objects = self::getAllBaseProviders();
 
       $configs = id(new PhabricatorAuthProviderConfigQuery())
         ->setViewer(PhabricatorUser::getOmnipotentUser())
         ->execute();
 
       $providers = array();
       foreach ($configs as $config) {
         if (!isset($objects[$config->getProviderClass()])) {
           // This configuration is for a provider which is not installed.
           continue;
         }
 
         $object = clone $objects[$config->getProviderClass()];
         $object->attachProviderConfig($config);
 
         $key = $object->getProviderKey();
         if (isset($providers[$key])) {
           throw new Exception(
             pht(
               "Two authentication providers use the same provider key ".
               "('%s'). Each provider must be identified by a unique key.",
               $key));
         }
         $providers[$key] = $object;
       }
     }
 
     return $providers;
   }
 
   public static function getAllEnabledProviders() {
     $providers = self::getAllProviders();
     foreach ($providers as $key => $provider) {
       if (!$provider->isEnabled()) {
         unset($providers[$key]);
       }
     }
     return $providers;
   }
 
   public static function getEnabledProviderByKey($provider_key) {
     return idx(self::getAllEnabledProviders(), $provider_key);
   }
 
   abstract public function getProviderName();
   abstract public function getAdapter();
 
   public function isEnabled() {
     return $this->getProviderConfig()->getIsEnabled();
   }
 
   public function shouldAllowLogin() {
     return $this->getProviderConfig()->getShouldAllowLogin();
   }
 
   public function shouldAllowRegistration() {
     return $this->getProviderConfig()->getShouldAllowRegistration();
   }
 
   public function shouldAllowAccountLink() {
     return $this->getProviderConfig()->getShouldAllowLink();
   }
 
   public function shouldAllowAccountUnlink() {
     return $this->getProviderConfig()->getShouldAllowUnlink();
   }
 
   public function shouldTrustEmails() {
     return $this->shouldAllowEmailTrustConfiguration() &&
            $this->getProviderConfig()->getShouldTrustEmails();
   }
 
   /**
    * Should we allow the adapter to be marked as "trusted". This is true for
    * all adapters except those that allow the user to type in emails (see
    * @{class:PhabricatorPasswordAuthProvider}).
    */
   public function shouldAllowEmailTrustConfiguration() {
     return true;
   }
 
   public function buildLoginForm(PhabricatorAuthStartController $controller) {
     return $this->renderLoginForm($controller->getRequest(), $mode = 'start');
   }
 
   public function buildInviteForm(PhabricatorAuthStartController $controller) {
     return $this->renderLoginForm($controller->getRequest(), $mode = 'invite');
   }
 
   abstract public function processLoginRequest(
     PhabricatorAuthLoginController $controller);
 
   public function buildLinkForm(PhabricatorAuthLinkController $controller) {
     return $this->renderLoginForm($controller->getRequest(), $mode = 'link');
   }
 
   public function shouldAllowAccountRefresh() {
     return true;
   }
 
   public function buildRefreshForm(
     PhabricatorAuthLinkController $controller) {
     return $this->renderLoginForm($controller->getRequest(), $mode = 'refresh');
   }
 
   protected function renderLoginForm(AphrontRequest $request, $mode) {
     throw new PhutilMethodNotImplementedException();
   }
 
   public function createProviders() {
     return array($this);
   }
 
   protected function willSaveAccount(PhabricatorExternalAccount $account) {
     return;
   }
 
   public function willRegisterAccount(PhabricatorExternalAccount $account) {
     return;
   }
 
   protected function loadOrCreateAccount($account_id) {
     if (!strlen($account_id)) {
       throw new Exception(pht('Empty account ID!'));
     }
 
     $adapter = $this->getAdapter();
     $adapter_class = get_class($adapter);
 
     if (!strlen($adapter->getAdapterType())) {
       throw new Exception(
         pht(
           "AuthAdapter (of class '%s') has an invalid implementation: ".
           "no adapter type.",
           $adapter_class));
     }
 
     if (!strlen($adapter->getAdapterDomain())) {
       throw new Exception(
         pht(
           "AuthAdapter (of class '%s') has an invalid implementation: ".
           "no adapter domain.",
           $adapter_class));
     }
 
     $account = id(new PhabricatorExternalAccount())->loadOneWhere(
       'accountType = %s AND accountDomain = %s AND accountID = %s',
       $adapter->getAdapterType(),
       $adapter->getAdapterDomain(),
       $account_id);
     if (!$account) {
       $account = id(new PhabricatorExternalAccount())
         ->setAccountType($adapter->getAdapterType())
         ->setAccountDomain($adapter->getAdapterDomain())
         ->setAccountID($account_id);
     }
 
     $account->setUsername($adapter->getAccountName());
     $account->setRealName($adapter->getAccountRealName());
     $account->setEmail($adapter->getAccountEmail());
     $account->setAccountURI($adapter->getAccountURI());
 
     $account->setProfileImagePHID(null);
     $image_uri = $adapter->getAccountImageURI();
     if ($image_uri) {
       try {
         $name = PhabricatorSlug::normalize($this->getProviderName());
         $name = $name.'-profile.jpg';
 
         // TODO: If the image has not changed, we do not need to make a new
         // file entry for it, but there's no convenient way to do this with
         // PhabricatorFile right now. The storage will get shared, so the impact
         // here is negligible.
         $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
           $image_file = PhabricatorFile::newFromFileDownload(
             $image_uri,
             array(
               'name' => $name,
               'viewPolicy' => PhabricatorPolicies::POLICY_NOONE,
             ));
           if ($image_file->isViewableImage()) {
             $image_file
               ->setViewPolicy(PhabricatorPolicies::getMostOpenPolicy())
               ->setCanCDN(true)
               ->save();
             $account->setProfileImagePHID($image_file->getPHID());
           } else {
             $image_file->delete();
           }
         unset($unguarded);
 
       } catch (Exception $ex) {
         // Log this but proceed, it's not especially important that we
         // be able to pull profile images.
         phlog($ex);
       }
     }
 
     $this->willSaveAccount($account);
 
     $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
     $account->save();
     unset($unguarded);
 
     return $account;
   }
 
   public function getLoginURI() {
     $app = PhabricatorApplication::getByClass('PhabricatorAuthApplication');
     return $app->getApplicationURI('/login/'.$this->getProviderKey().'/');
   }
 
   public function getSettingsURI() {
     return '/settings/panel/external/';
   }
 
   public function getStartURI() {
     $app = PhabricatorApplication::getByClass('PhabricatorAuthApplication');
     $uri = $app->getApplicationURI('/start/');
     return $uri;
   }
 
   public function isDefaultRegistrationProvider() {
     return false;
   }
 
   public function shouldRequireRegistrationPassword() {
     return false;
   }
 
   public function getDefaultExternalAccount() {
     throw new PhutilMethodNotImplementedException();
   }
 
   public function getLoginOrder() {
     return '500-'.$this->getProviderName();
   }
 
   protected function getLoginIcon() {
     return 'Generic';
   }
 
   public function isLoginFormAButton() {
     return false;
   }
 
   public function renderConfigPropertyTransactionTitle(
     PhabricatorAuthProviderConfigTransaction $xaction) {
 
     return null;
   }
 
   public function readFormValuesFromProvider() {
     return array();
   }
 
   public function readFormValuesFromRequest(AphrontRequest $request) {
     return array();
   }
 
   public function processEditForm(
     AphrontRequest $request,
     array $values) {
 
     $errors = array();
     $issues = array();
 
     return array($errors, $issues, $values);
   }
 
   public function extendEditForm(
     AphrontRequest $request,
     AphrontFormView $form,
     array $values,
     array $issues) {
 
     return;
   }
 
   public function willRenderLinkedAccount(
     PhabricatorUser $viewer,
     PHUIObjectItemView $item,
     PhabricatorExternalAccount $account) {
 
     $account_view = id(new PhabricatorAuthAccountView())
       ->setExternalAccount($account)
       ->setAuthProvider($this);
 
     $item->appendChild(
       phutil_tag(
         'div',
         array(
           'class' => 'mmr mml mst mmb',
         ),
         $account_view));
   }
 
   /**
    * Return true to use a two-step configuration (setup, configure) instead of
    * the default single-step configuration. In practice, this means that
    * creating a new provider instance will redirect back to the edit page
    * instead of the provider list.
    *
    * @return bool True if this provider uses two-step configuration.
    */
   public function hasSetupStep() {
     return false;
   }
 
   /**
    * Render a standard login/register button element.
    *
    * The `$attributes` parameter takes these keys:
    *
    *   - `uri`: URI the button should take the user to when clicked.
    *   - `method`: Optional HTTP method the button should use, defaults to GET.
    *
    * @param   AphrontRequest  HTTP request.
    * @param   string          Request mode string.
    * @param   map             Additional parameters, see above.
    * @return  wild            Login button.
    */
   protected function renderStandardLoginButton(
     AphrontRequest $request,
     $mode,
     array $attributes = array()) {
 
     PhutilTypeSpec::checkMap(
       $attributes,
       array(
         'method' => 'optional string',
         'uri' => 'string',
         'sigil' => 'optional string',
       ));
 
     $viewer = $request->getUser();
     $adapter = $this->getAdapter();
 
     if ($mode == 'link') {
       $button_text = pht('Link External Account');
     } else if ($mode == 'refresh') {
       $button_text = pht('Refresh Account Link');
     } else if ($mode == 'invite') {
       $button_text = pht('Register Account');
     } else if ($this->shouldAllowRegistration()) {
       $button_text = pht('Login or Register');
     } else {
       $button_text = pht('Login');
     }
 
     $icon = id(new PHUIIconView())
       ->setSpriteSheet(PHUIIconView::SPRITE_LOGIN)
       ->setSpriteIcon($this->getLoginIcon());
 
     $button = id(new PHUIButtonView())
       ->setSize(PHUIButtonView::BIG)
       ->setColor(PHUIButtonView::GREY)
       ->setIcon($icon)
       ->setText($button_text)
       ->setSubtext($this->getProviderName());
 
     $uri = $attributes['uri'];
     $uri = new PhutilURI($uri);
     $params = $uri->getQueryParams();
     $uri->setQueryParams(array());
 
     $content = array($button);
 
     foreach ($params as $key => $value) {
       $content[] = phutil_tag(
         'input',
         array(
           'type' => 'hidden',
           'name' => $key,
           'value' => $value,
         ));
     }
 
     return phabricator_form(
       $viewer,
       array(
         'method' => idx($attributes, 'method', 'GET'),
         'action' => (string)$uri,
         'sigil'  => idx($attributes, 'sigil'),
       ),
       $content);
   }
 
   public function renderConfigurationFooter() {
     return null;
   }
 
   public function getAuthCSRFCode(AphrontRequest $request) {
     $phcid = $request->getCookie(PhabricatorCookies::COOKIE_CLIENTID);
     if (!strlen($phcid)) {
       throw new Exception(
         pht(
           'Your browser did not submit a "%s" cookie with client state '.
           'information in the request. Check that cookies are enabled. '.
           'If this problem persists, you may need to clear your cookies.',
           PhabricatorCookies::COOKIE_CLIENTID));
     }
 
     return PhabricatorHash::digest($phcid);
   }
 
   protected function verifyAuthCSRFCode(AphrontRequest $request, $actual) {
     $expect = $this->getAuthCSRFCode($request);
 
     if (!strlen($actual)) {
       throw new Exception(
         pht(
           'The authentication provider did not return a client state '.
           'parameter in its response, but one was expected. If this '.
           'problem persists, you may need to clear your cookies.'));
     }
 
     if ($actual !== $expect) {
       throw new Exception(
         pht(
           'The authentication provider did not return the correct client '.
           'state parameter in its response. If this problem persists, you may '.
           'need to clear your cookies.'));
     }
   }
 
 }
diff --git a/src/applications/base/PhabricatorApplication.php b/src/applications/base/PhabricatorApplication.php
index 4c176641b..03f14a241 100644
--- a/src/applications/base/PhabricatorApplication.php
+++ b/src/applications/base/PhabricatorApplication.php
@@ -1,589 +1,602 @@
 <?php
 
 /**
  * @task  info  Application Information
  * @task  ui    UI Integration
  * @task  uri   URI Routing
  * @task  mail  Email integration
  * @task  fact  Fact Integration
  * @task  meta  Application Management
  */
-abstract class PhabricatorApplication implements PhabricatorPolicyInterface {
+abstract class PhabricatorApplication
+  extends Phobject
+  implements PhabricatorPolicyInterface {
 
   const MAX_STATUS_ITEMS      = 100;
 
   const GROUP_CORE            = 'core';
   const GROUP_UTILITIES       = 'util';
   const GROUP_ADMIN           = 'admin';
   const GROUP_DEVELOPER       = 'developer';
 
-  public static function getApplicationGroups() {
+  final public static function getApplicationGroups() {
     return array(
       self::GROUP_CORE          => pht('Core Applications'),
       self::GROUP_UTILITIES     => pht('Utilities'),
       self::GROUP_ADMIN         => pht('Administration'),
       self::GROUP_DEVELOPER     => pht('Developer Tools'),
     );
   }
 
 
 /* -(  Application Information  )-------------------------------------------- */
 
   abstract public function getName();
 
   public function getShortDescription() {
     return pht('%s Application', $this->getName());
   }
 
-  public function isInstalled() {
+  final public function isInstalled() {
     if (!$this->canUninstall()) {
       return true;
     }
 
     $prototypes = PhabricatorEnv::getEnvConfig('phabricator.show-prototypes');
     if (!$prototypes && $this->isPrototype()) {
       return false;
     }
 
     $uninstalled = PhabricatorEnv::getEnvConfig(
       'phabricator.uninstalled-applications');
 
     return empty($uninstalled[get_class($this)]);
   }
 
 
   public function isPrototype() {
     return false;
   }
 
 
   /**
    * Return `true` if this application should never appear in application lists
    * in the UI. Primarily intended for unit test applications or other
    * pseudo-applications.
    *
    * Few applications should be unlisted. For most applications, use
    * @{method:isLaunchable} to hide them from main launch views instead.
    *
    * @return bool True to remove application from UI lists.
    */
   public function isUnlisted() {
     return false;
   }
 
 
   /**
    * Return `true` if this application is a normal application with a base
    * URI and a web interface.
    *
    * Launchable applications can be pinned to the home page, and show up in the
    * "Launcher" view of the Applications application. Making an application
    * unlauncahble prevents pinning and hides it from this view.
    *
    * Usually, an application should be marked unlaunchable if:
    *
    *   - it is available on every page anyway (like search); or
    *   - it does not have a web interface (like subscriptions); or
    *   - it is still pre-release and being intentionally buried.
    *
    * To hide applications more completely, use @{method:isUnlisted}.
    *
    * @return bool True if the application is launchable.
    */
   public function isLaunchable() {
     return true;
   }
 
 
   /**
    * Return `true` if this application should be pinned by default.
    *
    * Users who have not yet set preferences see a default list of applications.
    *
    * @param PhabricatorUser User viewing the pinned application list.
    * @return bool True if this application should be pinned by default.
    */
   public function isPinnedByDefault(PhabricatorUser $viewer) {
     return false;
   }
 
 
   /**
    * Returns true if an application is first-party (developed by Phacility)
    * and false otherwise.
    *
    * @return bool True if this application is developed by Phacility.
    */
   final public function isFirstParty() {
     $where = id(new ReflectionClass($this))->getFileName();
     $root = phutil_get_library_root('phabricator');
 
     if (!Filesystem::isDescendant($where, $root)) {
       return false;
     }
 
     if (Filesystem::isDescendant($where, $root.'/extensions')) {
       return false;
     }
 
     return true;
   }
 
   public function canUninstall() {
     return true;
   }
 
-  public function getPHID() {
+  final public function getPHID() {
     return 'PHID-APPS-'.get_class($this);
   }
 
   public function getTypeaheadURI() {
     return $this->isLaunchable() ? $this->getBaseURI() : null;
   }
 
   public function getBaseURI() {
     return null;
   }
 
-  public function getApplicationURI($path = '') {
+  final public function getApplicationURI($path = '') {
     return $this->getBaseURI().ltrim($path, '/');
   }
 
   public function getIconURI() {
     return null;
   }
 
   public function getFontIcon() {
     return 'fa-puzzle-piece';
   }
 
   public function getApplicationOrder() {
     return PHP_INT_MAX;
   }
 
   public function getApplicationGroup() {
     return self::GROUP_CORE;
   }
 
   public function getTitleGlyph() {
     return null;
   }
 
-  public function getHelpMenuItems(PhabricatorUser $viewer) {
+  final public function getHelpMenuItems(PhabricatorUser $viewer) {
     $items = array();
 
     $articles = $this->getHelpDocumentationArticles($viewer);
     if ($articles) {
       $items[] = id(new PHUIListItemView())
         ->setType(PHUIListItemView::TYPE_LABEL)
         ->setName(pht('%s Documentation', $this->getName()));
       foreach ($articles as $article) {
         $item = id(new PHUIListItemView())
           ->setName($article['name'])
           ->setIcon('fa-book')
           ->setHref($article['href']);
 
         $items[] = $item;
       }
     }
 
     $command_specs = $this->getMailCommandObjects();
     if ($command_specs) {
       $items[] = id(new PHUIListItemView())
         ->setType(PHUIListItemView::TYPE_LABEL)
         ->setName(pht('Email Help'));
       foreach ($command_specs as $key => $spec) {
         $object = $spec['object'];
 
         $class = get_class($this);
         $href = '/applications/mailcommands/'.$class.'/'.$key.'/';
 
         $item = id(new PHUIListItemView())
           ->setName($spec['name'])
           ->setIcon('fa-envelope-o')
           ->setHref($href);
         $items[] = $item;
       }
     }
 
     return $items;
   }
 
   public function getHelpDocumentationArticles(PhabricatorUser $viewer) {
     return array();
   }
 
   public function getOverview() {
     return null;
   }
 
   public function getEventListeners() {
     return array();
   }
 
   public function getRemarkupRules() {
     return array();
   }
 
   public function getQuicksandURIPatternBlacklist() {
     return array();
   }
 
   public function getMailCommandObjects() {
     return array();
   }
 
 
 /* -(  URI Routing  )-------------------------------------------------------- */
 
 
   public function getRoutes() {
     return array();
   }
 
 
 /* -(  Email Integration  )-------------------------------------------------- */
 
 
   public function supportsEmailIntegration() {
     return false;
   }
 
-  protected function getInboundEmailSupportLink() {
+  final protected function getInboundEmailSupportLink() {
     return PhabricatorEnv::getDocLink('Configuring Inbound Email');
   }
 
   public function getAppEmailBlurb() {
     throw new PhutilMethodNotImplementedException();
   }
 
 
 /* -(  Fact Integration  )--------------------------------------------------- */
 
 
   public function getFactObjectsForAnalysis() {
     return array();
   }
 
 
 /* -(  UI Integration  )----------------------------------------------------- */
 
 
   /**
    * Render status elements (like "3 Waiting Reviews") for application list
    * views. These provide a way to alert users to new or pending action items
    * in applications.
    *
    * @param PhabricatorUser Viewing user.
    * @return list<PhabricatorApplicationStatusView> Application status elements.
    * @task ui
    */
   public function loadStatus(PhabricatorUser $user) {
     return array();
   }
 
   /**
    * @return string
    * @task ui
    */
-  public static function formatStatusCount(
+  final public static function formatStatusCount(
     $count,
     $limit_string = '%s',
     $base_string = '%d') {
     if ($count == self::MAX_STATUS_ITEMS) {
       $count_str = pht($limit_string, ($count - 1).'+');
     } else {
       $count_str = pht($base_string, $count);
     }
     return $count_str;
   }
 
 
   /**
    * You can provide an optional piece of flavor text for the application. This
    * is currently rendered in application launch views if the application has no
    * status elements.
    *
    * @return string|null Flavor text.
    * @task ui
    */
   public function getFlavorText() {
     return null;
   }
 
 
   /**
    * Build items for the main menu.
    *
    * @param  PhabricatorUser    The viewing user.
    * @param  AphrontController  The current controller. May be null for special
    *                            pages like 404, exception handlers, etc.
    * @return list<PHUIListItemView> List of menu items.
    * @task ui
    */
   public function buildMainMenuItems(
     PhabricatorUser $user,
     PhabricatorController $controller = null) {
     return array();
   }
 
 
   /**
    * Build extra items for the main menu. Generally, this is used to render
    * static dropdowns.
    *
    * @param  PhabricatorUser    The viewing user.
    * @param  AphrontController  The current controller. May be null for special
    *                            pages like 404, exception handlers, etc.
    * @return view               List of menu items.
    * @task ui
    */
   public function buildMainMenuExtraNodes(
     PhabricatorUser $viewer,
     PhabricatorController $controller = null) {
     return array();
   }
 
 
   /**
    * Build items for the "quick create" menu.
    *
    * @param   PhabricatorUser         The viewing user.
    * @return  list<PHUIListItemView>  List of menu items.
    */
   public function getQuickCreateItems(PhabricatorUser $viewer) {
     return array();
   }
 
 
 /* -(  Application Management  )--------------------------------------------- */
 
 
-  public static function getByClass($class_name) {
+  final public static function getByClass($class_name) {
     $selected = null;
     $applications = self::getAllApplications();
 
     foreach ($applications as $application) {
       if (get_class($application) == $class_name) {
         $selected = $application;
         break;
       }
     }
 
     if (!$selected) {
       throw new Exception(pht("No application '%s'!", $class_name));
     }
 
     return $selected;
   }
 
-  public static function getAllApplications() {
+  final public static function getAllApplications() {
     static $applications;
 
     if ($applications === null) {
       $apps = id(new PhutilSymbolLoader())
         ->setAncestorClass(__CLASS__)
         ->loadObjects();
 
       // Reorder the applications into "application order". Notably, this
       // ensures their event handlers register in application order.
       $apps = msort($apps, 'getApplicationOrder');
       $apps = mgroup($apps, 'getApplicationGroup');
 
       $group_order = array_keys(self::getApplicationGroups());
       $apps = array_select_keys($apps, $group_order) + $apps;
 
       $apps = array_mergev($apps);
 
       $applications = $apps;
     }
 
     return $applications;
   }
 
-  public static function getAllInstalledApplications() {
+  final public static function getAllInstalledApplications() {
     $all_applications = self::getAllApplications();
     $apps = array();
     foreach ($all_applications as $app) {
       if (!$app->isInstalled()) {
         continue;
       }
 
       $apps[] = $app;
     }
 
     return $apps;
   }
 
 
   /**
    * Determine if an application is installed, by application class name.
    *
    * To check if an application is installed //and// available to a particular
    * viewer, user @{method:isClassInstalledForViewer}.
    *
    * @param string  Application class name.
    * @return bool   True if the class is installed.
    * @task meta
    */
-  public static function isClassInstalled($class) {
+  final public static function isClassInstalled($class) {
     return self::getByClass($class)->isInstalled();
   }
 
 
   /**
    * Determine if an application is installed and available to a viewer, by
    * application class name.
    *
    * To check if an application is installed at all, use
    * @{method:isClassInstalled}.
    *
    * @param string Application class name.
    * @param PhabricatorUser Viewing user.
    * @return bool True if the class is installed for the viewer.
    * @task meta
    */
-  public static function isClassInstalledForViewer(
+  final public static function isClassInstalledForViewer(
     $class,
     PhabricatorUser $viewer) {
 
     if (!self::isClassInstalled($class)) {
       return false;
     }
 
     return PhabricatorPolicyFilter::hasCapability(
       $viewer,
       self::getByClass($class),
       PhabricatorPolicyCapability::CAN_VIEW);
   }
 
 
 /* -(  PhabricatorPolicyInterface  )----------------------------------------- */
 
 
   public function getCapabilities() {
     return array_merge(
       array(
         PhabricatorPolicyCapability::CAN_VIEW,
         PhabricatorPolicyCapability::CAN_EDIT,
       ),
       array_keys($this->getCustomCapabilities()));
   }
 
   public function getPolicy($capability) {
     $default = $this->getCustomPolicySetting($capability);
     if ($default) {
       return $default;
     }
 
     switch ($capability) {
       case PhabricatorPolicyCapability::CAN_VIEW:
         return PhabricatorPolicies::getMostOpenPolicy();
       case PhabricatorPolicyCapability::CAN_EDIT:
         return PhabricatorPolicies::POLICY_ADMIN;
       default:
         $spec = $this->getCustomCapabilitySpecification($capability);
         return idx($spec, 'default', PhabricatorPolicies::POLICY_USER);
     }
   }
 
   public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
     return false;
   }
 
   public function describeAutomaticCapability($capability) {
     return null;
   }
 
 
 /* -(  Policies  )----------------------------------------------------------- */
 
   protected function getCustomCapabilities() {
     return array();
   }
 
-  private function getCustomPolicySetting($capability) {
+  final private function getCustomPolicySetting($capability) {
     if (!$this->isCapabilityEditable($capability)) {
       return null;
     }
 
     $policy_locked = PhabricatorEnv::getEnvConfig('policy.locked');
     if (isset($policy_locked[$capability])) {
       return $policy_locked[$capability];
     }
 
     $config = PhabricatorEnv::getEnvConfig('phabricator.application-settings');
 
     $app = idx($config, $this->getPHID());
     if (!$app) {
       return null;
     }
 
     $policy = idx($app, 'policy');
     if (!$policy) {
       return null;
     }
 
     return idx($policy, $capability);
   }
 
 
-  private function getCustomCapabilitySpecification($capability) {
+  final private function getCustomCapabilitySpecification($capability) {
     $custom = $this->getCustomCapabilities();
     if (!isset($custom[$capability])) {
       throw new Exception(pht("Unknown capability '%s'!", $capability));
     }
     return $custom[$capability];
   }
 
-  public function getCapabilityLabel($capability) {
+  final public function getCapabilityLabel($capability) {
     switch ($capability) {
       case PhabricatorPolicyCapability::CAN_VIEW:
         return pht('Can Use Application');
       case PhabricatorPolicyCapability::CAN_EDIT:
         return pht('Can Configure Application');
     }
 
     $capobj = PhabricatorPolicyCapability::getCapabilityByKey($capability);
     if ($capobj) {
       return $capobj->getCapabilityName();
     }
 
     return null;
   }
 
-  public function isCapabilityEditable($capability) {
+  final public function isCapabilityEditable($capability) {
     switch ($capability) {
       case PhabricatorPolicyCapability::CAN_VIEW:
         return $this->canUninstall();
       case PhabricatorPolicyCapability::CAN_EDIT:
         return false;
       default:
         $spec = $this->getCustomCapabilitySpecification($capability);
         return idx($spec, 'edit', true);
     }
   }
 
-  public function getCapabilityCaption($capability) {
+  final public function getCapabilityCaption($capability) {
     switch ($capability) {
       case PhabricatorPolicyCapability::CAN_VIEW:
         if (!$this->canUninstall()) {
           return pht(
             'This application is required for Phabricator to operate, so all '.
             'users must have access to it.');
         } else {
           return null;
         }
       case PhabricatorPolicyCapability::CAN_EDIT:
         return null;
       default:
         $spec = $this->getCustomCapabilitySpecification($capability);
         return idx($spec, 'caption');
     }
   }
 
+  final public function getCapabilityTemplatePHIDType($capability) {
+    switch ($capability) {
+      case PhabricatorPolicyCapability::CAN_VIEW:
+      case PhabricatorPolicyCapability::CAN_EDIT:
+        return null;
+    }
+
+    $spec = $this->getCustomCapabilitySpecification($capability);
+    return idx($spec, 'template');
+  }
+
   public function getApplicationSearchDocumentTypes() {
     return array();
   }
 
 }
diff --git a/src/applications/base/__tests__/PhabricatorApplicationTestCase.php b/src/applications/base/__tests__/PhabricatorApplicationTestCase.php
new file mode 100644
index 000000000..ca01cba11
--- /dev/null
+++ b/src/applications/base/__tests__/PhabricatorApplicationTestCase.php
@@ -0,0 +1,10 @@
+<?php
+
+final class PhabricatorApplicationTestCase extends PhabricatorTestCase {
+
+  public function testGetAllApplications() {
+    PhabricatorApplication::getAllApplications();
+    $this->assertTrue(true);
+  }
+
+}
diff --git a/src/applications/cache/PhabricatorCaches.php b/src/applications/cache/PhabricatorCaches.php
index e3290172f..5824c6458 100644
--- a/src/applications/cache/PhabricatorCaches.php
+++ b/src/applications/cache/PhabricatorCaches.php
@@ -1,384 +1,384 @@
 <?php
 
 /**
  *
  * @task request    Request Cache
  * @task immutable  Immutable Cache
  * @task setup      Setup Cache
  * @task compress   Compression
  */
-final class PhabricatorCaches {
+final class PhabricatorCaches extends Phobject {
 
   private static $requestCache;
 
   public static function getNamespace() {
     return PhabricatorEnv::getEnvConfig('phabricator.cache-namespace');
   }
 
   private static function newStackFromCaches(array $caches) {
     $caches = self::addNamespaceToCaches($caches);
     $caches = self::addProfilerToCaches($caches);
     return id(new PhutilKeyValueCacheStack())
       ->setCaches($caches);
   }
 
 /* -(  Request Cache  )------------------------------------------------------ */
 
 
   /**
    * Get a request cache stack.
    *
    * This cache stack is destroyed after each logical request. In particular,
    * it is destroyed periodically by the daemons, while `static` caches are
    * not.
    *
    * @return PhutilKeyValueCacheStack Request cache stack.
    */
   public static function getRequestCache() {
     if (!self::$requestCache) {
       self::$requestCache = new PhutilInRequestKeyValueCache();
     }
     return self::$requestCache;
   }
 
 
   /**
    * Destroy the request cache.
    *
    * This is called at the beginning of each logical request.
    *
    * @return void
    */
   public static function destroyRequestCache() {
     self::$requestCache = null;
   }
 
 
 /* -(  Immutable Cache  )---------------------------------------------------- */
 
 
   /**
    * Gets an immutable cache stack.
    *
    * This stack trades mutability away for improved performance. Normally, it is
    * APC + DB.
    *
    * In the general case with multiple web frontends, this stack can not be
    * cleared, so it is only appropriate for use if the value of a given key is
    * permanent and immutable.
    *
    * @return PhutilKeyValueCacheStack Best immutable stack available.
    * @task immutable
    */
   public static function getImmutableCache() {
     static $cache;
     if (!$cache) {
       $caches = self::buildImmutableCaches();
       $cache = self::newStackFromCaches($caches);
     }
     return $cache;
   }
 
 
   /**
    * Build the immutable cache stack.
    *
    * @return list<PhutilKeyValueCache> List of caches.
    * @task immutable
    */
   private static function buildImmutableCaches() {
     $caches = array();
 
     $apc = new PhutilAPCKeyValueCache();
     if ($apc->isAvailable()) {
       $caches[] = $apc;
     }
 
     $caches[] = new PhabricatorKeyValueDatabaseCache();
 
     return $caches;
   }
 
 
 /* -(  Repository Graph Cache  )--------------------------------------------- */
 
 
   public static function getRepositoryGraphL1Cache() {
     static $cache;
     if (!$cache) {
       $caches = self::buildRepositoryGraphL1Caches();
       $cache = self::newStackFromCaches($caches);
     }
     return $cache;
   }
 
   private static function buildRepositoryGraphL1Caches() {
     $caches = array();
 
     $request = new PhutilInRequestKeyValueCache();
     $request->setLimit(32);
     $caches[] = $request;
 
     $apc = new PhutilAPCKeyValueCache();
     if ($apc->isAvailable()) {
       $caches[] = $apc;
     }
 
     return $caches;
   }
 
   public static function getRepositoryGraphL2Cache() {
     static $cache;
     if (!$cache) {
       $caches = self::buildRepositoryGraphL2Caches();
       $cache = self::newStackFromCaches($caches);
     }
     return $cache;
   }
 
   private static function buildRepositoryGraphL2Caches() {
     $caches = array();
     $caches[] = new PhabricatorKeyValueDatabaseCache();
     return $caches;
   }
 
 
 /* -(  Setup Cache  )-------------------------------------------------------- */
 
 
   /**
    * Highly specialized cache for performing setup checks. We use this cache
    * to determine if we need to run expensive setup checks when the page
    * loads. Without it, we would need to run these checks every time.
    *
    * Normally, this cache is just APC. In the absence of APC, this cache
    * degrades into a slow, quirky on-disk cache.
    *
    * NOTE: Do not use this cache for anything else! It is not a general-purpose
    * cache!
    *
    * @return PhutilKeyValueCacheStack Most qualified available cache stack.
    * @task setup
    */
   public static function getSetupCache() {
     static $cache;
     if (!$cache) {
       $caches = self::buildSetupCaches();
       $cache = self::newStackFromCaches($caches);
     }
     return $cache;
   }
 
 
   /**
    * @task setup
    */
   private static function buildSetupCaches() {
     // In most cases, we should have APC. This is an ideal cache for our
     // purposes -- it's fast and empties on server restart.
     $apc = new PhutilAPCKeyValueCache();
     if ($apc->isAvailable()) {
       return array($apc);
     }
 
     // If we don't have APC, build a poor approximation on disk. This is still
     // much better than nothing; some setup steps are quite slow.
     $disk_path = self::getSetupCacheDiskCachePath();
     if ($disk_path) {
       $disk = new PhutilOnDiskKeyValueCache();
       $disk->setCacheFile($disk_path);
       $disk->setWait(0.1);
       if ($disk->isAvailable()) {
         return array($disk);
       }
     }
 
     return array();
   }
 
 
   /**
    * @task setup
    */
   private static function getSetupCacheDiskCachePath() {
     // The difficulty here is in choosing a path which will change on server
     // restart (we MUST have this property), but as rarely as possible
     // otherwise (we desire this property to give the cache the best hit rate
     // we can).
 
     // In some setups, the parent PID is more stable and longer-lived that the
     // PID (e.g., under apache, our PID will be a worker while the ppid will
     // be the main httpd process). If we're confident we're running under such
     // a setup, we can try to use the PPID as the basis for our cache instead
     // of our own PID.
     $use_ppid = false;
 
     switch (php_sapi_name()) {
       case 'cli-server':
         // This is the PHP5.4+ built-in webserver. We should use the pid
         // (the server), not the ppid (probably a shell or something).
         $use_ppid = false;
         break;
       case 'fpm-fcgi':
         // We should be safe to use PPID here.
         $use_ppid = true;
         break;
       case 'apache2handler':
         // We're definitely safe to use the PPID.
         $use_ppid = true;
         break;
     }
 
     $pid_basis = getmypid();
     if ($use_ppid) {
       if (function_exists('posix_getppid')) {
         $parent_pid = posix_getppid();
         // On most systems, normal processes can never have PIDs lower than 100,
         // so something likely went wrong if we we get one of these.
         if ($parent_pid > 100) {
           $pid_basis = $parent_pid;
         }
       }
     }
 
     // If possible, we also want to know when the process launched, so we can
     // drop the cache if a process restarts but gets the same PID an earlier
     // process had. "/proc" is not available everywhere (e.g., not on OSX), but
     // check if we have it.
     $epoch_basis = null;
     $stat = @stat("/proc/{$pid_basis}");
     if ($stat !== false) {
       $epoch_basis = $stat['ctime'];
     }
 
     $tmp_dir = sys_get_temp_dir();
 
     $tmp_path = $tmp_dir.DIRECTORY_SEPARATOR.'phabricator-setup';
     if (!file_exists($tmp_path)) {
       @mkdir($tmp_path);
     }
 
     $is_ok = self::testTemporaryDirectory($tmp_path);
     if (!$is_ok) {
       $tmp_path = $tmp_dir;
       $is_ok = self::testTemporaryDirectory($tmp_path);
       if (!$is_ok) {
         // We can't find anywhere to write the cache, so just bail.
         return null;
       }
     }
 
     $tmp_name = 'setup-'.$pid_basis;
     if ($epoch_basis) {
       $tmp_name .= '.'.$epoch_basis;
     }
     $tmp_name .= '.cache';
 
     return $tmp_path.DIRECTORY_SEPARATOR.$tmp_name;
   }
 
 
   /**
    * @task setup
    */
   private static function testTemporaryDirectory($dir) {
     if (!@file_exists($dir)) {
       return false;
     }
     if (!@is_dir($dir)) {
       return false;
     }
     if (!@is_writable($dir)) {
       return false;
     }
 
     return true;
   }
 
   private static function addProfilerToCaches(array $caches) {
     foreach ($caches as $key => $cache) {
       $pcache = new PhutilKeyValueCacheProfiler($cache);
       $pcache->setProfiler(PhutilServiceProfiler::getInstance());
       $caches[$key] = $pcache;
     }
     return $caches;
   }
 
   private static function addNamespaceToCaches(array $caches) {
     $namespace = self::getNamespace();
     if (!$namespace) {
       return $caches;
     }
 
     foreach ($caches as $key => $cache) {
       $ncache = new PhutilKeyValueCacheNamespace($cache);
       $ncache->setNamespace($namespace);
       $caches[$key] = $ncache;
     }
 
     return $caches;
   }
 
 
   /**
    * Deflate a value, if deflation is available and has an impact.
    *
    * If the value is larger than 1KB, we have `gzdeflate()`, we successfully
    * can deflate it, and it benefits from deflation, we deflate it. Otherwise
    * we leave it as-is.
    *
    * Data can later be inflated with @{method:inflateData}.
    *
    * @param string String to attempt to deflate.
    * @return string|null Deflated string, or null if it was not deflated.
    * @task compress
    */
   public static function maybeDeflateData($value) {
     $len = strlen($value);
     if ($len <= 1024) {
       return null;
     }
 
     if (!function_exists('gzdeflate')) {
       return null;
     }
 
     $deflated = gzdeflate($value);
     if ($deflated === false) {
       return null;
     }
 
     $deflated_len = strlen($deflated);
     if ($deflated_len >= ($len / 2)) {
       return null;
     }
 
     return $deflated;
   }
 
 
   /**
    * Inflate data previously deflated by @{method:maybeDeflateData}.
    *
    * @param string Deflated data, from @{method:maybeDeflateData}.
    * @return string Original, uncompressed data.
    * @task compress
    */
   public static function inflateData($value) {
     if (!function_exists('gzinflate')) {
       throw new Exception(
         pht(
           '%s is not available; unable to read deflated data!',
           'gzinflate()'));
     }
 
     $value = gzinflate($value);
     if ($value === false) {
       throw new Exception(pht('Failed to inflate data!'));
     }
 
     return $value;
   }
 
 
 }
diff --git a/src/applications/calendar/controller/PhabricatorCalendarEventEditController.php b/src/applications/calendar/controller/PhabricatorCalendarEventEditController.php
index ccf7cbf0b..218fe1737 100644
--- a/src/applications/calendar/controller/PhabricatorCalendarEventEditController.php
+++ b/src/applications/calendar/controller/PhabricatorCalendarEventEditController.php
@@ -1,605 +1,604 @@
 <?php
 
 final class PhabricatorCalendarEventEditController
   extends PhabricatorCalendarController {
 
   private $id;
 
   public function willProcessRequest(array $data) {
     $this->id = idx($data, 'id');
   }
 
   public function isCreate() {
     return !$this->id;
   }
 
   public function handleRequest(AphrontRequest $request) {
     $viewer = $request->getViewer();
     $user_phid = $viewer->getPHID();
     $error_name = true;
-    $error_recurrence_end_date = true;
+    $error_recurrence_end_date = null;
     $error_start_date = true;
     $error_end_date = true;
     $validation_exception = null;
 
     $is_recurring_id = celerity_generate_unique_node_id();
     $recurrence_end_date_id = celerity_generate_unique_node_id();
     $frequency_id = celerity_generate_unique_node_id();
     $all_day_id = celerity_generate_unique_node_id();
     $start_date_id = celerity_generate_unique_node_id();
     $end_date_id = celerity_generate_unique_node_id();
 
     $next_workflow = $request->getStr('next');
     $uri_query = $request->getStr('query');
 
     if ($this->isCreate()) {
       $mode = $request->getStr('mode');
       $event = PhabricatorCalendarEvent::initializeNewCalendarEvent(
         $viewer,
         $mode);
 
       $create_start_year = $request->getInt('year');
       $create_start_month = $request->getInt('month');
       $create_start_day = $request->getInt('day');
       $create_start_time = $request->getStr('time');
 
       if ($create_start_year) {
         $start = AphrontFormDateControlValue::newFromParts(
           $viewer,
           $create_start_year,
           $create_start_month,
           $create_start_day,
           $create_start_time);
         if (!$start->isValid()) {
           return new Aphront400Response();
         }
         $start_value = AphrontFormDateControlValue::newFromEpoch(
           $viewer,
           $start->getEpoch());
 
         $end = clone $start_value->getDateTime();
         $end->modify('+1 hour');
         $end_value = AphrontFormDateControlValue::newFromEpoch(
           $viewer,
           $end->format('U'));
 
       } else {
         list($start_value, $end_value) = $this->getDefaultTimeValues($viewer);
       }
 
       $recurrence_end_date_value = clone $end_value;
       $recurrence_end_date_value->setOptional(true);
 
       $submit_label = pht('Create');
       $page_title = pht('Create Event');
       $redirect = 'created';
       $subscribers = array();
       $invitees = array($user_phid);
       $cancel_uri = $this->getApplicationURI();
     } else {
       $event = id(new PhabricatorCalendarEventQuery())
       ->setViewer($viewer)
       ->withIDs(array($this->id))
       ->requireCapabilities(
         array(
           PhabricatorPolicyCapability::CAN_VIEW,
           PhabricatorPolicyCapability::CAN_EDIT,
         ))
       ->executeOne();
 
       if (!$event) {
         return new Aphront404Response();
       }
 
       if ($request->getURIData('sequence')) {
         $index = $request->getURIData('sequence');
 
         $result = $this->getEventAtIndexForGhostPHID(
           $viewer,
           $event->getPHID(),
           $index);
 
         if ($result) {
           return id(new AphrontRedirectResponse())
             ->setURI('/calendar/event/edit/'.$result->getID().'/');
         }
 
         $event = $this->createEventFromGhost(
           $viewer,
           $event,
           $index);
 
         return id(new AphrontRedirectResponse())
           ->setURI('/calendar/event/edit/'.$event->getID().'/');
       }
 
       $end_value = AphrontFormDateControlValue::newFromEpoch(
         $viewer,
         $event->getDateTo());
       $start_value = AphrontFormDateControlValue::newFromEpoch(
         $viewer,
         $event->getDateFrom());
       $recurrence_end_date_value = id(clone $end_value)
         ->setOptional(true);
 
       $submit_label = pht('Update');
       $page_title   = pht('Update Event');
 
       $subscribers = PhabricatorSubscribersQuery::loadSubscribersForPHID(
         $event->getPHID());
 
       $invitees = array();
       foreach ($event->getInvitees() as $invitee) {
         if ($invitee->isUninvited()) {
           continue;
         } else {
           $invitees[] = $invitee->getInviteePHID();
         }
       }
 
       $cancel_uri = '/'.$event->getMonogram();
     }
 
     $name = $event->getName();
     $description = $event->getDescription();
     $is_all_day = $event->getIsAllDay();
     $is_recurring = $event->getIsRecurring();
     $is_parent = $event->getIsRecurrenceParent();
     $frequency = idx($event->getRecurrenceFrequency(), 'rule');
     $icon = $event->getIcon();
 
     $current_policies = id(new PhabricatorPolicyQuery())
       ->setViewer($viewer)
       ->setObject($event)
       ->execute();
 
     if ($request->isFormPost()) {
       $xactions = array();
       $name = $request->getStr('name');
 
       $start_value = AphrontFormDateControlValue::newFromRequest(
         $request,
         'start');
       $end_value = AphrontFormDateControlValue::newFromRequest(
         $request,
         'end');
       $recurrence_end_date_value = AphrontFormDateControlValue::newFromRequest(
         $request,
         'recurrenceEndDate');
       $recurrence_end_date_value->setOptional(true);
       $description = $request->getStr('description');
       $subscribers = $request->getArr('subscribers');
       $edit_policy = $request->getStr('editPolicy');
       $view_policy = $request->getStr('viewPolicy');
       $is_recurring = $request->getStr('isRecurring') ? 1 : 0;
       $frequency = $request->getStr('frequency');
       $is_all_day = $request->getStr('isAllDay');
       $icon = $request->getStr('icon');
 
       $invitees = $request->getArr('invitees');
       $new_invitees = $this->getNewInviteeList($invitees, $event);
       $status_attending = PhabricatorCalendarEventInvitee::STATUS_ATTENDING;
       if ($this->isCreate()) {
         $status = idx($new_invitees, $viewer->getPHID());
         if ($status) {
           $new_invitees[$viewer->getPHID()] = $status_attending;
         }
       }
 
       $xactions[] = id(new PhabricatorCalendarEventTransaction())
         ->setTransactionType(
           PhabricatorCalendarEventTransaction::TYPE_NAME)
         ->setNewValue($name);
 
       if ($is_parent && $this->isCreate()) {
         $xactions[] = id(new PhabricatorCalendarEventTransaction())
           ->setTransactionType(
             PhabricatorCalendarEventTransaction::TYPE_RECURRING)
           ->setNewValue($is_recurring);
 
         $xactions[] = id(new PhabricatorCalendarEventTransaction())
           ->setTransactionType(
             PhabricatorCalendarEventTransaction::TYPE_FREQUENCY)
           ->setNewValue(array('rule' => $frequency));
 
         if (!$recurrence_end_date_value->isDisabled()) {
           $xactions[] = id(new PhabricatorCalendarEventTransaction())
             ->setTransactionType(
               PhabricatorCalendarEventTransaction::TYPE_RECURRENCE_END_DATE)
             ->setNewValue($recurrence_end_date_value);
         }
       }
 
       if (($is_parent && $this->isCreate()) || !$is_parent) {
         $xactions[] = id(new PhabricatorCalendarEventTransaction())
           ->setTransactionType(
             PhabricatorCalendarEventTransaction::TYPE_ALL_DAY)
           ->setNewValue($is_all_day);
 
         $xactions[] = id(new PhabricatorCalendarEventTransaction())
           ->setTransactionType(
             PhabricatorCalendarEventTransaction::TYPE_ICON)
           ->setNewValue($icon);
 
         $xactions[] = id(new PhabricatorCalendarEventTransaction())
           ->setTransactionType(
             PhabricatorCalendarEventTransaction::TYPE_START_DATE)
           ->setNewValue($start_value);
 
         $xactions[] = id(new PhabricatorCalendarEventTransaction())
           ->setTransactionType(
             PhabricatorCalendarEventTransaction::TYPE_END_DATE)
           ->setNewValue($end_value);
       }
 
 
       $xactions[] = id(new PhabricatorCalendarEventTransaction())
         ->setTransactionType(
           PhabricatorTransactions::TYPE_SUBSCRIBERS)
         ->setNewValue(array('=' => array_fuse($subscribers)));
 
       $xactions[] = id(new PhabricatorCalendarEventTransaction())
         ->setTransactionType(
           PhabricatorCalendarEventTransaction::TYPE_INVITE)
         ->setNewValue($new_invitees);
 
       $xactions[] = id(new PhabricatorCalendarEventTransaction())
         ->setTransactionType(
           PhabricatorCalendarEventTransaction::TYPE_DESCRIPTION)
         ->setNewValue($description);
 
       $xactions[] = id(new PhabricatorCalendarEventTransaction())
         ->setTransactionType(PhabricatorTransactions::TYPE_VIEW_POLICY)
         ->setNewValue($request->getStr('viewPolicy'));
 
       $xactions[] = id(new PhabricatorCalendarEventTransaction())
         ->setTransactionType(PhabricatorTransactions::TYPE_EDIT_POLICY)
         ->setNewValue($request->getStr('editPolicy'));
 
       $editor = id(new PhabricatorCalendarEventEditor())
         ->setActor($viewer)
         ->setContentSourceFromRequest($request)
         ->setContinueOnNoEffect(true);
 
       try {
         $xactions = $editor->applyTransactions($event, $xactions);
         $response = id(new AphrontRedirectResponse());
         switch ($next_workflow) {
           case 'day':
             if (!$uri_query) {
               $uri_query = 'month';
             }
             $year = $start_value->getDateTime()->format('Y');
             $month = $start_value->getDateTime()->format('m');
             $day = $start_value->getDateTime()->format('d');
             $response->setURI(
               '/calendar/query/'.$uri_query.'/'.$year.'/'.$month.'/'.$day.'/');
             break;
           default:
             $response->setURI('/E'.$event->getID());
             break;
         }
         return $response;
       } catch (PhabricatorApplicationTransactionValidationException $ex) {
         $validation_exception = $ex;
         $error_name = $ex->getShortMessage(
             PhabricatorCalendarEventTransaction::TYPE_NAME);
         $error_start_date = $ex->getShortMessage(
             PhabricatorCalendarEventTransaction::TYPE_START_DATE);
         $error_end_date = $ex->getShortMessage(
             PhabricatorCalendarEventTransaction::TYPE_END_DATE);
         $error_recurrence_end_date = $ex->getShortMessage(
             PhabricatorCalendarEventTransaction::TYPE_RECURRENCE_END_DATE);
 
         $event->setViewPolicy($view_policy);
         $event->setEditPolicy($edit_policy);
       }
     }
 
     $is_recurring_checkbox = null;
     $recurrence_end_date_control = null;
     $recurrence_frequency_select = null;
 
     $all_day_checkbox = null;
     $start_control = null;
     $end_control = null;
 
     $recurring_date_edit_label = null;
 
     $name = id(new AphrontFormTextControl())
       ->setLabel(pht('Name'))
       ->setName('name')
       ->setValue($name)
       ->setError($error_name);
 
     if ($this->isCreate()) {
       Javelin::initBehavior('recurring-edit', array(
         'isRecurring' => $is_recurring_id,
         'frequency' => $frequency_id,
         'recurrenceEndDate' => $recurrence_end_date_id,
       ));
 
       $is_recurring_checkbox = id(new AphrontFormCheckboxControl())
         ->addCheckbox(
           'isRecurring',
           1,
           pht('Recurring Event'),
           $is_recurring,
           $is_recurring_id);
 
       $recurrence_end_date_control = id(new AphrontFormDateControl())
         ->setUser($viewer)
         ->setName('recurrenceEndDate')
         ->setLabel(pht('Recurrence End Date'))
         ->setError($error_recurrence_end_date)
         ->setValue($recurrence_end_date_value)
         ->setID($recurrence_end_date_id)
         ->setIsTimeDisabled(true)
         ->setIsDisabled($recurrence_end_date_value->isDisabled())
-        ->setAllowNull(true)
-        ->isRequired(false);
+        ->setAllowNull(true);
 
       $recurrence_frequency_select = id(new AphrontFormSelectControl())
         ->setName('frequency')
         ->setOptions(array(
             'daily' => pht('Daily'),
             'weekly' => pht('Weekly'),
             'monthly' => pht('Monthly'),
             'yearly' => pht('Yearly'),
           ))
         ->setValue($frequency)
         ->setLabel(pht('Recurring Event Frequency'))
         ->setID($frequency_id)
         ->setDisabled(!$is_recurring);
     }
 
     if ($this->isCreate() || (!$is_parent && !$this->isCreate())) {
       Javelin::initBehavior('event-all-day', array(
         'allDayID' => $all_day_id,
         'startDateID' => $start_date_id,
         'endDateID' => $end_date_id,
       ));
 
       $all_day_checkbox = id(new AphrontFormCheckboxControl())
         ->addCheckbox(
           'isAllDay',
           1,
           pht('All Day Event'),
           $is_all_day,
           $all_day_id);
 
       $start_control = id(new AphrontFormDateControl())
         ->setUser($viewer)
         ->setName('start')
         ->setLabel(pht('Start'))
         ->setError($error_start_date)
         ->setValue($start_value)
         ->setID($start_date_id)
         ->setIsTimeDisabled($is_all_day)
         ->setEndDateID($end_date_id);
 
       $end_control = id(new AphrontFormDateControl())
         ->setUser($viewer)
         ->setName('end')
         ->setLabel(pht('End'))
         ->setError($error_end_date)
         ->setValue($end_value)
         ->setID($end_date_id)
         ->setIsTimeDisabled($is_all_day);
     } else if ($is_parent) {
       $recurring_date_edit_label = id(new AphrontFormStaticControl())
         ->setUser($viewer)
         ->setValue(pht('Date and time of recurring event cannot be edited.'));
 
       if (!$recurrence_end_date_value->isDisabled()) {
         $disabled_recurrence_end_date_value =
           $recurrence_end_date_value->getValueAsFormat('M d, Y');
         $recurrence_end_date_control = id(new AphrontFormStaticControl())
           ->setUser($viewer)
           ->setLabel(pht('Recurrence End Date'))
           ->setValue($disabled_recurrence_end_date_value)
           ->setDisabled(true);
       }
 
       $recurrence_frequency_select = id(new AphrontFormSelectControl())
         ->setName('frequency')
         ->setOptions(array(
             'daily' => pht('Daily'),
             'weekly' => pht('Weekly'),
             'monthly' => pht('Monthly'),
             'yearly' => pht('Yearly'),
           ))
         ->setValue($frequency)
         ->setLabel(pht('Recurring Event Frequency'))
         ->setID($frequency_id)
         ->setDisabled(true);
 
       $all_day_checkbox = id(new AphrontFormCheckboxControl())
         ->addCheckbox(
           'isAllDay',
           1,
           pht('All Day Event'),
           $is_all_day,
           $all_day_id)
         ->setDisabled(true);
 
       $start_disabled = $start_value->getValueAsFormat('M d, Y, g:i A');
       $end_disabled = $end_value->getValueAsFormat('M d, Y, g:i A');
 
       $start_control = id(new AphrontFormStaticControl())
         ->setUser($viewer)
         ->setLabel(pht('Start'))
         ->setValue($start_disabled)
         ->setDisabled(true);
 
       $end_control = id(new AphrontFormStaticControl())
         ->setUser($viewer)
         ->setLabel(pht('End'))
         ->setValue($end_disabled);
     }
 
     $description = id(new AphrontFormTextAreaControl())
       ->setLabel(pht('Description'))
       ->setName('description')
       ->setValue($description);
 
     $view_policies = id(new AphrontFormPolicyControl())
       ->setUser($viewer)
       ->setCapability(PhabricatorPolicyCapability::CAN_VIEW)
       ->setPolicyObject($event)
       ->setPolicies($current_policies)
       ->setName('viewPolicy');
     $edit_policies = id(new AphrontFormPolicyControl())
       ->setUser($viewer)
       ->setCapability(PhabricatorPolicyCapability::CAN_EDIT)
       ->setPolicyObject($event)
       ->setPolicies($current_policies)
       ->setName('editPolicy');
 
     $subscribers = id(new AphrontFormTokenizerControl())
       ->setLabel(pht('Subscribers'))
       ->setName('subscribers')
       ->setValue($subscribers)
       ->setUser($viewer)
       ->setDatasource(new PhabricatorMetaMTAMailableDatasource());
 
     $invitees = id(new AphrontFormTokenizerControl())
       ->setLabel(pht('Invitees'))
       ->setName('invitees')
       ->setValue($invitees)
       ->setUser($viewer)
       ->setDatasource(new PhabricatorMetaMTAMailableDatasource());
 
     if ($this->isCreate()) {
       $icon_uri = $this->getApplicationURI('icon/');
     } else {
       $icon_uri = $this->getApplicationURI('icon/'.$event->getID().'/');
     }
     $icon_display = PhabricatorCalendarIcon::renderIconForChooser($icon);
     $icon = id(new AphrontFormChooseButtonControl())
       ->setLabel(pht('Icon'))
       ->setName('icon')
       ->setDisplayValue($icon_display)
       ->setButtonText(pht('Choose Icon...'))
       ->setChooseURI($icon_uri)
       ->setValue($icon);
 
     $form = id(new AphrontFormView())
       ->addHiddenInput('next', $next_workflow)
       ->addHiddenInput('query', $uri_query)
       ->setUser($viewer)
       ->appendChild($name);
 
     if ($recurring_date_edit_label) {
       $form->appendControl($recurring_date_edit_label);
     }
     if ($is_recurring_checkbox) {
       $form->appendChild($is_recurring_checkbox);
     }
     if ($recurrence_end_date_control) {
       $form->appendChild($recurrence_end_date_control);
     }
     if ($recurrence_frequency_select) {
       $form->appendControl($recurrence_frequency_select);
     }
 
     $form
       ->appendChild($all_day_checkbox)
       ->appendChild($start_control)
       ->appendChild($end_control)
       ->appendControl($view_policies)
       ->appendControl($edit_policies)
       ->appendControl($subscribers)
       ->appendControl($invitees)
       ->appendChild($description)
       ->appendChild($icon);
 
 
     if ($request->isAjax()) {
       return $this->newDialog()
         ->setTitle($page_title)
         ->setWidth(AphrontDialogView::WIDTH_FULL)
         ->appendForm($form)
         ->addCancelButton($cancel_uri)
         ->addSubmitButton($submit_label);
     }
 
     $submit = id(new AphrontFormSubmitControl())
       ->addCancelButton($cancel_uri)
       ->setValue($submit_label);
 
     $form->appendChild($submit);
 
     $form_box = id(new PHUIObjectBoxView())
       ->setHeaderText($page_title)
       ->setForm($form);
 
     $crumbs = $this->buildApplicationCrumbs();
 
     if (!$this->isCreate()) {
       $crumbs->addTextCrumb('E'.$event->getId(), '/E'.$event->getId());
     }
 
     $crumbs->addTextCrumb($page_title);
 
     $object_box = id(new PHUIObjectBoxView())
       ->setHeaderText($page_title)
       ->setValidationException($validation_exception)
       ->appendChild($form);
 
     return $this->buildApplicationPage(
       array(
         $crumbs,
         $object_box,
         ),
       array(
         'title' => $page_title,
       ));
   }
 
 
   public function getNewInviteeList(array $phids, $event) {
     $invitees = $event->getInvitees();
     $invitees = mpull($invitees, null, 'getInviteePHID');
     $invited_status = PhabricatorCalendarEventInvitee::STATUS_INVITED;
     $uninvited_status = PhabricatorCalendarEventInvitee::STATUS_UNINVITED;
     $phids = array_fuse($phids);
 
     $new = array();
     foreach ($phids as $phid) {
       $old_status = $event->getUserInviteStatus($phid);
       if ($old_status != $uninvited_status) {
         continue;
       }
       $new[$phid] = $invited_status;
     }
 
     foreach ($invitees as $invitee) {
       $deleted_invitee = !idx($phids, $invitee->getInviteePHID());
       if ($deleted_invitee) {
         $new[$invitee->getInviteePHID()] = $uninvited_status;
       }
     }
 
     return $new;
   }
 
   private function getDefaultTimeValues($viewer) {
     $start = new DateTime('@'.time());
     $start->setTimeZone($viewer->getTimeZone());
 
     $start->setTime($start->format('H'), 0, 0);
     $start->modify('+1 hour');
     $end = id(clone $start)->modify('+1 hour');
 
     $start_value = AphrontFormDateControlValue::newFromEpoch(
       $viewer,
       $start->format('U'));
     $end_value = AphrontFormDateControlValue::newFromEpoch(
       $viewer,
       $end->format('U'));
 
     return array($start_value, $end_value);
   }
 
 }
diff --git a/src/applications/calendar/storage/PhabricatorCalendarEvent.php b/src/applications/calendar/storage/PhabricatorCalendarEvent.php
index ea6c6a48e..f12bf9a9c 100644
--- a/src/applications/calendar/storage/PhabricatorCalendarEvent.php
+++ b/src/applications/calendar/storage/PhabricatorCalendarEvent.php
@@ -1,524 +1,524 @@
 <?php
 
 final class PhabricatorCalendarEvent extends PhabricatorCalendarDAO
   implements PhabricatorPolicyInterface,
   PhabricatorMarkupInterface,
   PhabricatorApplicationTransactionInterface,
   PhabricatorSubscribableInterface,
   PhabricatorTokenReceiverInterface,
   PhabricatorDestructibleInterface,
   PhabricatorMentionableInterface,
   PhabricatorFlaggableInterface {
 
   protected $name;
   protected $userPHID;
   protected $dateFrom;
   protected $dateTo;
   protected $description;
   protected $isCancelled;
   protected $isAllDay;
   protected $icon;
   protected $mailKey;
 
   protected $isRecurring = 0;
   protected $recurrenceFrequency = array();
   protected $recurrenceEndDate;
 
   private $isGhostEvent = false;
   protected $instanceOfEventPHID;
   protected $sequenceIndex;
 
   protected $viewPolicy;
   protected $editPolicy;
 
   const DEFAULT_ICON = 'fa-calendar';
 
   private $parentEvent = self::ATTACHABLE;
   private $invitees = self::ATTACHABLE;
   private $appliedViewer;
 
   public static function initializeNewCalendarEvent(
     PhabricatorUser $actor,
     $mode) {
     $app = id(new PhabricatorApplicationQuery())
       ->setViewer($actor)
       ->withClasses(array('PhabricatorCalendarApplication'))
       ->executeOne();
 
     $view_policy = null;
     $is_recurring = 0;
 
     if ($mode == 'public') {
       $view_policy = PhabricatorPolicies::getMostOpenPolicy();
     } else if ($mode == 'recurring') {
       $is_recurring = true;
     } else {
       $view_policy = $actor->getPHID();
     }
 
     return id(new PhabricatorCalendarEvent())
       ->setUserPHID($actor->getPHID())
       ->setIsCancelled(0)
       ->setIsAllDay(0)
       ->setIsRecurring($is_recurring)
       ->setIcon(self::DEFAULT_ICON)
       ->setViewPolicy($view_policy)
       ->setEditPolicy($actor->getPHID())
       ->attachInvitees(array())
       ->applyViewerTimezone($actor);
   }
 
   public function applyViewerTimezone(PhabricatorUser $viewer) {
     if ($this->appliedViewer) {
       throw new Exception(pht('Viewer timezone is already applied!'));
     }
 
     $this->appliedViewer = $viewer;
 
     if (!$this->getIsAllDay()) {
       return $this;
     }
 
     $zone = $viewer->getTimeZone();
 
 
     $this->setDateFrom(
       $this->getDateEpochForTimeZone(
         $this->getDateFrom(),
         new DateTimeZone('Pacific/Kiritimati'),
         'Y-m-d',
         null,
         $zone));
 
     $this->setDateTo(
       $this->getDateEpochForTimeZone(
         $this->getDateTo(),
         new DateTimeZone('Pacific/Midway'),
         'Y-m-d 23:59:00',
         '-1 day',
         $zone));
 
     return $this;
   }
 
 
   public function removeViewerTimezone(PhabricatorUser $viewer) {
     if (!$this->appliedViewer) {
       throw new Exception(pht('Viewer timezone is not applied!'));
     }
 
     if ($viewer->getPHID() != $this->appliedViewer->getPHID()) {
       throw new Exception(pht('Removed viewer must match applied viewer!'));
     }
 
     $this->appliedViewer = null;
 
     if (!$this->getIsAllDay()) {
       return $this;
     }
 
     $zone = $viewer->getTimeZone();
 
     $this->setDateFrom(
       $this->getDateEpochForTimeZone(
         $this->getDateFrom(),
         $zone,
         'Y-m-d',
         null,
         new DateTimeZone('Pacific/Kiritimati')));
 
     $this->setDateTo(
       $this->getDateEpochForTimeZone(
         $this->getDateTo(),
         $zone,
         'Y-m-d',
         '+1 day',
         new DateTimeZone('Pacific/Midway')));
 
     return $this;
   }
 
   private function getDateEpochForTimeZone(
     $epoch,
     $src_zone,
     $format,
     $adjust,
     $dst_zone) {
 
     $src = new DateTime('@'.$epoch);
     $src->setTimeZone($src_zone);
 
     if (strlen($adjust)) {
       $adjust = ' '.$adjust;
     }
 
     $dst = new DateTime($src->format($format).$adjust, $dst_zone);
     return $dst->format('U');
   }
 
   public function save() {
     if ($this->appliedViewer) {
       throw new Exception(
         pht(
           'Can not save event with viewer timezone still applied!'));
     }
 
     if (!$this->mailKey) {
       $this->mailKey = Filesystem::readRandomCharacters(20);
     }
 
     return parent::save();
   }
 
   /**
    * Get the event start epoch for evaluating invitee availability.
    *
    * When assessing availability, we pretend events start earlier than they
    * really. This allows us to mark users away for the entire duration of a
    * series of back-to-back meetings, even if they don't strictly overlap.
    *
    * @return int Event start date for availability caches.
    */
   public function getDateFromForCache() {
     return ($this->getDateFrom() - phutil_units('15 minutes in seconds'));
   }
 
   protected function getConfiguration() {
     return array(
       self::CONFIG_AUX_PHID => true,
       self::CONFIG_COLUMN_SCHEMA => array(
         'name' => 'text',
         'dateFrom' => 'epoch',
         'dateTo' => 'epoch',
         'description' => 'text',
         'isCancelled' => 'bool',
         'isAllDay' => 'bool',
         'icon' => 'text32',
         'mailKey' => 'bytes20',
         'isRecurring' => 'bool',
         'recurrenceEndDate' => 'epoch?',
         'instanceOfEventPHID' => 'phid?',
         'sequenceIndex' => 'uint32?',
       ),
       self::CONFIG_KEY_SCHEMA => array(
         'userPHID_dateFrom' => array(
           'columns' => array('userPHID', 'dateTo'),
         ),
         'key_instance' => array(
           'columns' => array('instanceOfEventPHID', 'sequenceIndex'),
           'unique' => true,
         ),
       ),
       self::CONFIG_SERIALIZATION => array(
         'recurrenceFrequency' => self::SERIALIZATION_JSON,
       ),
     ) + parent::getConfiguration();
   }
 
   public function generatePHID() {
     return PhabricatorPHID::generateNewPHID(
       PhabricatorCalendarEventPHIDType::TYPECONST);
   }
 
   public function getMonogram() {
     return 'E'.$this->getID();
   }
 
   public function getInvitees() {
     return $this->assertAttached($this->invitees);
   }
 
   public function attachInvitees(array $invitees) {
     $this->invitees = $invitees;
     return $this;
   }
 
   public function getUserInviteStatus($phid) {
     $invitees = $this->getInvitees();
     $invitees = mpull($invitees, null, 'getInviteePHID');
 
     $invited = idx($invitees, $phid);
     if (!$invited) {
       return PhabricatorCalendarEventInvitee::STATUS_UNINVITED;
     }
     $invited = $invited->getStatus();
     return $invited;
   }
 
   public function getIsUserAttending($phid) {
     $attending_status = PhabricatorCalendarEventInvitee::STATUS_ATTENDING;
 
     $old_status = $this->getUserInviteStatus($phid);
     $is_attending = ($old_status == $attending_status);
 
     return $is_attending;
   }
 
   public function getIsUserInvited($phid) {
     $uninvited_status = PhabricatorCalendarEventInvitee::STATUS_UNINVITED;
     $declined_status = PhabricatorCalendarEventInvitee::STATUS_DECLINED;
     $status = $this->getUserInviteStatus($phid);
     if ($status == $uninvited_status || $status == $declined_status) {
       return false;
     }
     return true;
   }
 
   public function getIsGhostEvent() {
     return $this->isGhostEvent;
   }
 
   public function setIsGhostEvent($is_ghost_event) {
     $this->isGhostEvent = $is_ghost_event;
     return $this;
   }
 
   public function generateNthGhost(
     $sequence_index,
     PhabricatorUser $actor) {
 
     $frequency = $this->getFrequencyUnit();
     $modify_key = '+'.$sequence_index.' '.$frequency;
 
     $instance_of = ($this->getPHID()) ?
       $this->getPHID() : $this->instanceOfEventPHID;
 
     $date = $this->dateFrom;
     $date_time = PhabricatorTime::getDateTimeFromEpoch($date, $actor);
     $date_time->modify($modify_key);
     $date = $date_time->format('U');
 
     $duration = $this->dateTo - $this->dateFrom;
 
     $edit_policy = PhabricatorPolicies::POLICY_NOONE;
 
     $ghost_event = id(clone $this)
       ->setIsGhostEvent(true)
       ->setDateFrom($date)
       ->setDateTo($date + $duration)
       ->setIsRecurring(true)
       ->setRecurrenceFrequency($this->recurrenceFrequency)
       ->setInstanceOfEventPHID($instance_of)
       ->setSequenceIndex($sequence_index)
       ->setEditPolicy($edit_policy);
 
     return $ghost_event;
   }
 
   public function getFrequencyUnit() {
     $frequency = idx($this->recurrenceFrequency, 'rule');
 
     switch ($frequency) {
       case 'daily':
         return 'day';
       case 'weekly':
         return 'week';
       case 'monthly':
         return 'month';
       case 'yearly':
-        return 'yearly';
+        return 'year';
       default:
         return 'day';
     }
   }
 
   public function getURI() {
     $uri = '/'.$this->getMonogram();
     if ($this->isGhostEvent) {
       $uri = $uri.'/'.$this->sequenceIndex;
     }
     return $uri;
   }
 
   public function getParentEvent() {
     return $this->assertAttached($this->parentEvent);
   }
 
   public function attachParentEvent($event) {
     $this->parentEvent = $event;
     return $this;
   }
 
   public function getIsCancelled() {
     $instance_of = $this->instanceOfEventPHID;
     if ($instance_of != null && $this->getIsParentCancelled()) {
       return true;
     }
     return $this->isCancelled;
   }
 
   public function getIsRecurrenceParent() {
     if ($this->isRecurring && !$this->instanceOfEventPHID) {
       return true;
     }
     return false;
   }
 
   public function getIsRecurrenceException() {
     if ($this->instanceOfEventPHID && !$this->isGhostEvent) {
       return true;
     }
     return false;
   }
 
   public function getIsParentCancelled() {
     if ($this->instanceOfEventPHID == null) {
       return false;
     }
 
     $recurring_event = $this->getParentEvent();
     if ($recurring_event->getIsCancelled()) {
       return true;
     }
     return false;
   }
 
 /* -(  Markup Interface  )--------------------------------------------------- */
 
 
   /**
    * @task markup
    */
   public function getMarkupFieldKey($field) {
     $hash = PhabricatorHash::digest($this->getMarkupText($field));
     $id = $this->getID();
     return "calendar:T{$id}:{$field}:{$hash}";
   }
 
 
   /**
    * @task markup
    */
   public function getMarkupText($field) {
     return $this->getDescription();
   }
 
 
   /**
    * @task markup
    */
   public function newMarkupEngine($field) {
     return PhabricatorMarkupEngine::newCalendarMarkupEngine();
   }
 
 
   /**
    * @task markup
    */
   public function didMarkupText(
     $field,
     $output,
     PhutilMarkupEngine $engine) {
     return $output;
   }
 
 
   /**
    * @task markup
    */
   public function shouldUseMarkupCache($field) {
     return (bool)$this->getID();
   }
 
 /* -(  PhabricatorPolicyInterface  )----------------------------------------- */
 
 
   public function getCapabilities() {
     return array(
       PhabricatorPolicyCapability::CAN_VIEW,
       PhabricatorPolicyCapability::CAN_EDIT,
     );
   }
 
   public function getPolicy($capability) {
     switch ($capability) {
       case PhabricatorPolicyCapability::CAN_VIEW:
         return $this->getViewPolicy();
       case PhabricatorPolicyCapability::CAN_EDIT:
         return $this->getEditPolicy();
     }
   }
 
   public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
     // The owner of a task can always view and edit it.
     $user_phid = $this->getUserPHID();
     if ($user_phid) {
       $viewer_phid = $viewer->getPHID();
       if ($viewer_phid == $user_phid) {
         return true;
       }
     }
 
     if ($capability == PhabricatorPolicyCapability::CAN_VIEW) {
       $status = $this->getUserInviteStatus($viewer->getPHID());
       if ($status == PhabricatorCalendarEventInvitee::STATUS_INVITED ||
         $status == PhabricatorCalendarEventInvitee::STATUS_ATTENDING ||
         $status == PhabricatorCalendarEventInvitee::STATUS_DECLINED) {
         return true;
       }
     }
 
     return false;
   }
 
   public function describeAutomaticCapability($capability) {
     return pht('The owner of an event can always view and edit it,
       and invitees can always view it, except if the event is an
       instance of a recurring event.');
   }
 
 /* -(  PhabricatorApplicationTransactionInterface  )------------------------- */
 
 
   public function getApplicationTransactionEditor() {
     return new PhabricatorCalendarEventEditor();
   }
 
   public function getApplicationTransactionObject() {
     return $this;
   }
 
   public function getApplicationTransactionTemplate() {
     return new PhabricatorCalendarEventTransaction();
   }
 
   public function willRenderTimeline(
     PhabricatorApplicationTransactionView $timeline,
     AphrontRequest $request) {
 
     return $timeline;
   }
 
 /* -(  PhabricatorSubscribableInterface  )----------------------------------- */
 
 
   public function isAutomaticallySubscribed($phid) {
     return ($phid == $this->getUserPHID());
   }
 
   public function shouldShowSubscribersProperty() {
     return true;
   }
 
   public function shouldAllowSubscription($phid) {
     return true;
   }
 
 /* -(  PhabricatorTokenReceiverInterface  )---------------------------------- */
 
 
   public function getUsersToNotifyOfTokenGiven() {
     return array($this->getUserPHID());
   }
 
 /* -(  PhabricatorDestructibleInterface  )----------------------------------- */
 
 
   public function destroyObjectPermanently(
     PhabricatorDestructionEngine $engine) {
 
     $this->openTransaction();
     $this->delete();
     $this->saveTransaction();
   }
 }
diff --git a/src/applications/calendar/util/CalendarTimeUtil.php b/src/applications/calendar/util/CalendarTimeUtil.php
index 71edb6946..0fc4f2e52 100644
--- a/src/applications/calendar/util/CalendarTimeUtil.php
+++ b/src/applications/calendar/util/CalendarTimeUtil.php
@@ -1,89 +1,89 @@
 <?php
 /**
  * This class is useful for generating various time objects, relative to the
  * user and their timezone.
  *
  * For now, the class exposes two sets of static methods for the two main
  * calendar views - one for the conpherence calendar widget and one for the
  * user profile calendar view. These have slight differences such as
  * conpherence showing both a three day "today 'til 2 days from now" *and*
  * a Sunday -> Saturday list, whilest the profile view shows a more simple
  * seven day rolling list of events.
  */
-final class CalendarTimeUtil {
+final class CalendarTimeUtil extends Phobject {
 
   public static function getCalendarEventEpochs(
     PhabricatorUser $user,
     $start_day_str = 'Sunday',
     $days = 9) {
 
     $objects = self::getStartDateTimeObjects($user, $start_day_str);
     $start_day = $objects['start_day'];
     $end_day = clone $start_day;
     $end_day->modify('+'.$days.' days');
 
     return array(
       'start_epoch' => $start_day->format('U'),
       'end_epoch' => $end_day->format('U'),
     );
   }
 
   public static function getCalendarWeekTimestamps(
     PhabricatorUser $user) {
     return self::getTimestamps($user, 'Today', 7);
   }
 
   public static function getCalendarWidgetTimestamps(
     PhabricatorUser $user) {
     return self::getTimestamps($user, 'Sunday', 9);
   }
 
   /**
    * Public for testing purposes only. You should probably use one of the
    * functions above.
    */
   public static function getTimestamps(
     PhabricatorUser $user,
     $start_day_str,
     $days) {
 
     $objects = self::getStartDateTimeObjects($user, $start_day_str);
     $start_day = $objects['start_day'];
     $timestamps = array();
     for ($day = 0; $day < $days; $day++) {
       $timestamp = clone $start_day;
       $timestamp->modify(sprintf('+%d days', $day));
       $timestamps[] = $timestamp;
     }
     return array(
       'today' => $objects['today'],
       'epoch_stamps' => $timestamps,
     );
   }
 
   private static function getStartDateTimeObjects(
     PhabricatorUser $user,
     $start_day_str) {
     $timezone = new DateTimeZone($user->getTimezoneIdentifier());
 
     $today_epoch = PhabricatorTime::parseLocalTime('today', $user);
     $today = new DateTime('@'.$today_epoch);
     $today->setTimeZone($timezone);
 
     if (strtolower($start_day_str) == 'today' ||
         $today->format('l') == $start_day_str) {
       $start_day = clone $today;
     } else {
       $start_epoch = PhabricatorTime::parseLocalTime(
         'last '.$start_day_str,
         $user);
       $start_day = new DateTime('@'.$start_epoch);
       $start_day->setTimeZone($timezone);
     }
     return array(
       'today' => $today,
       'start_day' => $start_day,
     );
   }
 
 }
diff --git a/src/applications/celerity/CelerityAPI.php b/src/applications/celerity/CelerityAPI.php
index d5649471a..2d5100ad1 100644
--- a/src/applications/celerity/CelerityAPI.php
+++ b/src/applications/celerity/CelerityAPI.php
@@ -1,18 +1,18 @@
 <?php
 
 /**
  * Indirection layer which provisions for a terrifying future where we need to
  * build multiple resource responses per page.
  */
-final class CelerityAPI {
+final class CelerityAPI extends Phobject {
 
   private static $response;
 
   public static function getStaticResourceResponse() {
     if (empty(self::$response)) {
       self::$response = new CelerityStaticResourceResponse();
     }
     return self::$response;
   }
 
 }
diff --git a/src/applications/celerity/CelerityResourceMap.php b/src/applications/celerity/CelerityResourceMap.php
index 7e5e00594..03ccfe607 100644
--- a/src/applications/celerity/CelerityResourceMap.php
+++ b/src/applications/celerity/CelerityResourceMap.php
@@ -1,260 +1,261 @@
 <?php
 
 /**
  * Interface to the static resource map, which is a graph of available
  * resources, resource dependencies, and packaging information. You generally do
  * not need to invoke it directly; instead, you call higher-level Celerity APIs
  * and it uses the resource map to satisfy your requests.
  */
-final class CelerityResourceMap {
+final class CelerityResourceMap extends Phobject {
 
   private static $instances = array();
 
   private $resources;
   private $symbolMap;
   private $requiresMap;
   private $packageMap;
   private $nameMap;
   private $hashMap;
+  private $componentMap;
 
   public function __construct(CelerityResources $resources) {
     $this->resources = $resources;
 
     $map = $resources->loadMap();
     $this->symbolMap = idx($map, 'symbols', array());
     $this->requiresMap = idx($map, 'requires', array());
     $this->packageMap = idx($map, 'packages', array());
     $this->nameMap = idx($map, 'names', array());
 
     // We derive these reverse maps at runtime.
 
     $this->hashMap = array_flip($this->nameMap);
     $this->componentMap = array();
     foreach ($this->packageMap as $package_name => $symbols) {
       foreach ($symbols as $symbol) {
         $this->componentMap[$symbol] = $package_name;
       }
     }
   }
 
   public static function getNamedInstance($name) {
     if (empty(self::$instances[$name])) {
       $resources_list = CelerityPhysicalResources::getAll();
       if (empty($resources_list[$name])) {
         throw new Exception(
           pht(
             'No resource source exists with name "%s"!',
             $name));
       }
 
       $instance = new CelerityResourceMap($resources_list[$name]);
       self::$instances[$name] = $instance;
     }
 
     return self::$instances[$name];
   }
 
   public function getNameMap() {
     return $this->nameMap;
   }
 
   public function getSymbolMap() {
     return $this->symbolMap;
   }
 
   public function getRequiresMap() {
     return $this->requiresMap;
   }
 
   public function getPackageMap() {
     return $this->packageMap;
   }
 
   public function getPackagedNamesForSymbols(array $symbols) {
     $resolved = $this->resolveResources($symbols);
     return $this->packageResources($resolved);
   }
 
   private function resolveResources(array $symbols) {
     $map = array();
     foreach ($symbols as $symbol) {
       if (!empty($map[$symbol])) {
         continue;
       }
       $this->resolveResource($map, $symbol);
     }
 
     return $map;
   }
 
   private function resolveResource(array &$map, $symbol) {
     if (empty($this->symbolMap[$symbol])) {
       throw new Exception(
         pht(
           'Attempting to resolve unknown resource, "%s".',
           $symbol));
     }
 
     $hash = $this->symbolMap[$symbol];
 
     $map[$symbol] = $hash;
 
     if (isset($this->requiresMap[$hash])) {
       $requires = $this->requiresMap[$hash];
     } else {
       $requires = array();
     }
 
     foreach ($requires as $required_symbol) {
       if (!empty($map[$required_symbol])) {
         continue;
       }
       $this->resolveResource($map, $required_symbol);
     }
   }
 
   private function packageResources(array $resolved_map) {
     $packaged = array();
     $handled = array();
     foreach ($resolved_map as $symbol => $hash) {
       if (isset($handled[$symbol])) {
         continue;
       }
 
       if (empty($this->componentMap[$symbol])) {
         $packaged[] = $this->hashMap[$hash];
       } else {
         $package_name = $this->componentMap[$symbol];
         $packaged[] = $package_name;
 
         $package_symbols = $this->packageMap[$package_name];
         foreach ($package_symbols as $package_symbol) {
           $handled[$package_symbol] = true;
         }
       }
     }
 
     return $packaged;
   }
 
   public function getResourceDataForName($resource_name) {
     return $this->resources->getResourceData($resource_name);
   }
 
   public function getResourceNamesForPackageName($package_name) {
     $package_symbols = idx($this->packageMap, $package_name);
     if (!$package_symbols) {
       return null;
     }
 
     $resource_names = array();
     foreach ($package_symbols as $symbol) {
       $resource_names[] = $this->hashMap[$this->symbolMap[$symbol]];
     }
 
     return $resource_names;
   }
 
 
   /**
    * Get the epoch timestamp of the last modification time of a symbol.
    *
    * @param string Resource symbol to lookup.
    * @return int Epoch timestamp of last resource modification.
    */
   public function getModifiedTimeForName($name) {
     if ($this->isPackageResource($name)) {
       $names = array();
       foreach ($this->packageMap[$name] as $symbol) {
         $names[] = $this->getResourceNameForSymbol($symbol);
       }
     } else {
       $names = array($name);
     }
 
     $mtime = 0;
     foreach ($names as $name) {
       $mtime = max($mtime, $this->resources->getResourceModifiedTime($name));
     }
 
     return $mtime;
   }
 
 
   /**
    * Return the absolute URI for the resource associated with a symbol. This
    * method is fairly low-level and ignores packaging.
    *
    * @param string Resource symbol to lookup.
    * @return string|null Resource URI, or null if the symbol is unknown.
    */
   public function getURIForSymbol($symbol) {
     $hash = idx($this->symbolMap, $symbol);
     return $this->getURIForHash($hash);
   }
 
 
   /**
    * Return the absolute URI for the resource associated with a resource name.
    * This method is fairly low-level and ignores packaging.
    *
    * @param string Resource name to lookup.
    * @return string|null  Resource URI, or null if the name is unknown.
    */
   public function getURIForName($name) {
     $hash = idx($this->nameMap, $name);
     return $this->getURIForHash($hash);
   }
 
 
   /**
    * Return the absolute URI for a resource, identified by hash.
    * This method is fairly low-level and ignores packaging.
    *
    * @param string Resource hash to lookup.
    * @return string|null Resource URI, or null if the hash is unknown.
    */
   private function getURIForHash($hash) {
     if ($hash === null) {
       return null;
     }
     return $this->resources->getResourceURI($hash, $this->hashMap[$hash]);
   }
 
 
   /**
    * Return the resource symbols required by a named resource.
    *
    * @param string Resource name to lookup.
    * @return list<string>|null  List of required symbols, or null if the name
    *                            is unknown.
    */
   public function getRequiredSymbolsForName($name) {
     $hash = idx($this->nameMap, $name);
     if ($hash === null) {
       return null;
     }
     return idx($this->requiresMap, $hash, array());
   }
 
 
   /**
    * Return the resource name for a given symbol.
    *
    * @param string Resource symbol to lookup.
    * @return string|null Resource name, or null if the symbol is unknown.
    */
   public function getResourceNameForSymbol($symbol) {
     $hash = idx($this->symbolMap, $symbol);
     return idx($this->hashMap, $hash);
   }
 
   public function isPackageResource($name) {
     return isset($this->packageMap[$name]);
   }
 
   public function getResourceTypeForName($name) {
     return $this->resources->getResourceType($name);
   }
 
 }
diff --git a/src/applications/celerity/CelerityResourceMapGenerator.php b/src/applications/celerity/CelerityResourceMapGenerator.php
index b9c4ddaa3..a5bb657bd 100644
--- a/src/applications/celerity/CelerityResourceMapGenerator.php
+++ b/src/applications/celerity/CelerityResourceMapGenerator.php
@@ -1,367 +1,367 @@
 <?php
 
-final class CelerityResourceMapGenerator {
+final class CelerityResourceMapGenerator extends Phobject {
 
   private $debug = false;
   private $resources;
 
   private $nameMap     = array();
   private $symbolMap   = array();
   private $requiresMap = array();
   private $packageMap  = array();
 
   public function __construct(CelerityPhysicalResources $resources) {
     $this->resources = $resources;
   }
 
   public function getNameMap() {
     return $this->nameMap;
   }
 
   public function getSymbolMap() {
     return $this->symbolMap;
   }
 
   public function getRequiresMap() {
     return $this->requiresMap;
   }
 
   public function getPackageMap() {
     return $this->packageMap;
   }
 
   public function setDebug($debug) {
     $this->debug = $debug;
     return $this;
   }
 
   protected function log($message) {
     if ($this->debug) {
       $console = PhutilConsole::getConsole();
       $console->writeErr("%s\n", $message);
     }
   }
 
   public function generate() {
     $binary_map = $this->rebuildBinaryResources($this->resources);
 
     $this->log(pht('Found %d binary resources.', count($binary_map)));
 
     $xformer = id(new CelerityResourceTransformer())
       ->setMinify(false)
       ->setRawURIMap(ipull($binary_map, 'uri'));
 
     $text_map = $this->rebuildTextResources($this->resources, $xformer);
 
     $this->log(pht('Found %d text resources.', count($text_map)));
 
     $resource_graph = array();
     $requires_map = array();
     $symbol_map = array();
     foreach ($text_map as $name => $info) {
       if (isset($info['provides'])) {
         $symbol_map[$info['provides']] = $info['hash'];
 
         // We only need to check for cycles and add this to the requires map
         // if it actually requires anything.
         if (!empty($info['requires'])) {
           $resource_graph[$info['provides']] = $info['requires'];
           $requires_map[$info['hash']] = $info['requires'];
         }
       }
     }
 
     $this->detectGraphCycles($resource_graph);
     $name_map = ipull($binary_map, 'hash') + ipull($text_map, 'hash');
     $hash_map = array_flip($name_map);
 
     $package_map = $this->rebuildPackages(
       $this->resources,
       $symbol_map,
       $hash_map);
 
     $this->log(pht('Found %d packages.', count($package_map)));
 
     $component_map = array();
     foreach ($package_map as $package_name => $package_info) {
       foreach ($package_info['symbols'] as $symbol) {
         $component_map[$symbol] = $package_name;
       }
     }
 
     $name_map = $this->mergeNameMaps(
       array(
         array(pht('Binary'), ipull($binary_map, 'hash')),
         array(pht('Text'), ipull($text_map, 'hash')),
         array(pht('Package'), ipull($package_map, 'hash')),
       ));
     $package_map = ipull($package_map, 'symbols');
 
     ksort($name_map, SORT_STRING);
     ksort($symbol_map, SORT_STRING);
     ksort($requires_map, SORT_STRING);
     ksort($package_map, SORT_STRING);
 
     $this->nameMap     = $name_map;
     $this->symbolMap   = $symbol_map;
     $this->requiresMap = $requires_map;
     $this->packageMap  = $package_map;
 
     return $this;
   }
 
   public function write() {
     $map_content = $this->formatMapContent(array(
       'names'    => $this->getNameMap(),
       'symbols'  => $this->getSymbolMap(),
       'requires' => $this->getRequiresMap(),
       'packages' => $this->getPackageMap(),
     ));
 
     $map_path = $this->resources->getPathToMap();
     $this->log(pht('Writing map "%s".', Filesystem::readablePath($map_path)));
     Filesystem::writeFile($map_path, $map_content);
 
     return $this;
   }
 
   private function formatMapContent(array $data) {
     $content = phutil_var_export($data);
     $generated = '@'.'generated';
 
     return <<<EOFILE
 <?php
 
 /**
  * This file is automatically generated. Use 'bin/celerity map' to rebuild it.
  *
  * {$generated}
  */
 return {$content};
 
 EOFILE;
   }
 
   /**
    * Find binary resources (like PNG and SWF) and return information about
    * them.
    *
    * @param CelerityPhysicalResources Resource map to find binary resources for.
    * @return map<string, map<string, string>> Resource information map.
    */
   private function rebuildBinaryResources(
     CelerityPhysicalResources $resources) {
 
     $binary_map = $resources->findBinaryResources();
     $result_map = array();
 
     foreach ($binary_map as $name => $data_hash) {
       $hash = $resources->getCelerityHash($data_hash.$name);
 
       $result_map[$name] = array(
         'hash' => $hash,
         'uri'  => $resources->getResourceURI($hash, $name),
       );
     }
 
     return $result_map;
   }
 
   /**
    * Find text resources (like JS and CSS) and return information about them.
    *
    * @param CelerityPhysicalResources Resource map to find text resources for.
    * @param CelerityResourceTransformer Configured resource transformer.
    * @return map<string, map<string, string>> Resource information map.
    */
   private function rebuildTextResources(
     CelerityPhysicalResources $resources,
     CelerityResourceTransformer $xformer) {
 
     $text_map = $resources->findTextResources();
     $result_map = array();
 
     foreach ($text_map as $name => $data_hash) {
       $raw_data = $resources->getResourceData($name);
       $xformed_data = $xformer->transformResource($name, $raw_data);
 
       $data_hash = $resources->getCelerityHash($xformed_data);
       $hash = $resources->getCelerityHash($data_hash.$name);
 
       list($provides, $requires) = $this->getProvidesAndRequires(
         $name,
         $raw_data);
 
       $result_map[$name] = array(
         'hash' => $hash,
       );
 
       if ($provides !== null) {
         $result_map[$name] += array(
           'provides' => $provides,
           'requires' => $requires,
         );
       }
     }
 
     return $result_map;
   }
 
   /**
    * Parse the `@provides` and `@requires` symbols out of a text resource, like
    * JS or CSS.
    *
    * @param string Resource name.
    * @param string Resource data.
    * @return pair<string|null, list<string>|null> The `@provides` symbol and
    *    the list of `@requires` symbols. If the resource is not part of the
    *    dependency graph, both are null.
    */
   private function getProvidesAndRequires($name, $data) {
     $parser = new PhutilDocblockParser();
 
     $matches = array();
     $ok = preg_match('@/[*][*].*?[*]/@s', $data, $matches);
     if (!$ok) {
       throw new Exception(
         pht(
           'Resource "%s" does not have a header doc comment. Encode '.
           'dependency data in a header docblock.',
           $name));
     }
 
     list($description, $metadata) = $parser->parse($matches[0]);
 
     $provides = preg_split('/\s+/', trim(idx($metadata, 'provides')));
     $requires = preg_split('/\s+/', trim(idx($metadata, 'requires')));
     $provides = array_filter($provides);
     $requires = array_filter($requires);
 
     if (!$provides) {
       // Tests and documentation-only JS is permitted to @provide no targets.
       return array(null, null);
     }
 
     if (count($provides) > 1) {
       throw new Exception(
         pht(
           'Resource "%s" must %s at most one Celerity target.',
           $name,
           '@provide'));
     }
 
     return array(head($provides), $requires);
   }
 
   /**
    * Check for dependency cycles in the resource graph. Raises an exception if
    * a cycle is detected.
    *
    * @param map<string, list<string>> Map of `@provides` symbols to their
    *                                  `@requires` symbols.
    * @return void
    */
   private function detectGraphCycles(array $nodes) {
     $graph = id(new CelerityResourceGraph())
       ->addNodes($nodes)
       ->setResourceGraph($nodes)
       ->loadGraph();
 
     foreach ($nodes as $provides => $requires) {
       $cycle = $graph->detectCycles($provides);
       if ($cycle) {
         throw new Exception(
           pht(
             'Cycle detected in resource graph: %s',
             implode(' > ', $cycle)));
       }
     }
   }
 
   /**
    * Build package specifications for a given resource source.
    *
    * @param CelerityPhysicalResources Resource source to rebuild.
    * @param map<string, string> Map of `@provides` to hashes.
    * @param map<string, string> Map of hashes to resource names.
    * @return map<string, map<string, string>> Package information maps.
    */
   private function rebuildPackages(
     CelerityPhysicalResources $resources,
     array $symbol_map,
     array $reverse_map) {
 
     $package_map = array();
 
     $package_spec = $resources->getResourcePackages();
     foreach ($package_spec as $package_name => $package_symbols) {
       $type = null;
       $hashes = array();
       foreach ($package_symbols as $symbol) {
         $symbol_hash = idx($symbol_map, $symbol);
         if ($symbol_hash === null) {
           throw new Exception(
             pht(
               'Package specification for "%s" includes "%s", but that symbol '.
               'is not %s by any resource.',
               $package_name,
               $symbol,
               '@provided'));
         }
 
         $resource_name = $reverse_map[$symbol_hash];
         $resource_type = $resources->getResourceType($resource_name);
         if ($type === null) {
           $type = $resource_type;
         } else if ($type !== $resource_type) {
           throw new Exception(
             pht(
               'Package specification for "%s" includes resources of multiple '.
               'types (%s, %s). Each package may only contain one type of '.
               'resource.',
               $package_name,
               $type,
               $resource_type));
         }
 
         $hashes[] = $symbol.':'.$symbol_hash;
       }
 
       $hash = $resources->getCelerityHash(implode("\n", $hashes));
       $package_map[$package_name] = array(
         'hash' => $hash,
         'symbols' => $package_symbols,
       );
     }
 
     return $package_map;
   }
 
   private function mergeNameMaps(array $maps) {
     $result = array();
     $origin = array();
 
     foreach ($maps as $map) {
       list($map_name, $data) = $map;
       foreach ($data as $name => $hash) {
         if (empty($result[$name])) {
           $result[$name] = $hash;
           $origin[$name] = $map_name;
         } else {
           $old = $origin[$name];
           $new = $map_name;
           throw new Exception(
             pht(
               'Resource source defines two resources with the same name, '.
               '"%s". One is defined in the "%s" map; the other in the "%s" '.
               'map. Each resource must have a unique name.',
               $name,
               $old,
               $new));
         }
       }
     }
     return $result;
   }
 
 }
diff --git a/src/applications/celerity/CelerityResourceTransformer.php b/src/applications/celerity/CelerityResourceTransformer.php
index 70163737a..a27840002 100644
--- a/src/applications/celerity/CelerityResourceTransformer.php
+++ b/src/applications/celerity/CelerityResourceTransformer.php
@@ -1,407 +1,407 @@
 <?php
 
-final class CelerityResourceTransformer {
+final class CelerityResourceTransformer extends Phobject {
 
   private $minify;
   private $rawURIMap;
   private $celerityMap;
   private $translateURICallback;
   private $currentPath;
 
   public function setTranslateURICallback($translate_uricallback) {
     $this->translateURICallback = $translate_uricallback;
     return $this;
   }
 
   public function setMinify($minify) {
     $this->minify = $minify;
     return $this;
   }
 
   public function setCelerityMap(CelerityResourceMap $celerity_map) {
     $this->celerityMap = $celerity_map;
     return $this;
   }
 
   public function setRawURIMap(array $raw_urimap) {
     $this->rawURIMap = $raw_urimap;
     return $this;
   }
 
   public function getRawURIMap() {
     return $this->rawURIMap;
   }
 
   /**
    * @phutil-external-symbol function jsShrink
    */
   public function transformResource($path, $data) {
     $type = self::getResourceType($path);
 
     switch ($type) {
       case 'css':
         $data = $this->replaceCSSPrintRules($path, $data);
         $data = $this->replaceCSSVariables($path, $data);
         $data = preg_replace_callback(
           '@url\s*\((\s*[\'"]?.*?)\)@s',
           nonempty(
             $this->translateURICallback,
             array($this, 'translateResourceURI')),
           $data);
         break;
     }
 
     if (!$this->minify) {
       return $data;
     }
 
     // Some resources won't survive minification (like Raphael.js), and are
     // marked so as not to be minified.
     if (strpos($data, '@'.'do-not-minify') !== false) {
       return $data;
     }
 
     switch ($type) {
       case 'css':
         // Remove comments.
         $data = preg_replace('@/\*.*?\*/@s', '', $data);
         // Remove whitespace around symbols.
         $data = preg_replace('@\s*([{}:;,])\s*@', '\1', $data);
         // Remove unnecessary semicolons.
         $data = preg_replace('@;}@', '}', $data);
         // Replace #rrggbb with #rgb when possible.
         $data = preg_replace(
           '@#([a-f0-9])\1([a-f0-9])\2([a-f0-9])\3@i',
           '#\1\2\3',
           $data);
         $data = trim($data);
         break;
       case 'js':
 
         // If `jsxmin` is available, use it. jsxmin is the Javelin minifier and
         // produces the smallest output, but is complicated to build.
         if (Filesystem::binaryExists('jsxmin')) {
           $future = new ExecFuture('jsxmin __DEV__:0');
           $future->write($data);
           list($err, $result) = $future->resolve();
           if (!$err) {
             $data = $result;
             break;
           }
         }
 
         // If `jsxmin` is not available, use `JsShrink`, which doesn't compress
         // quite as well but is always available.
         $root = dirname(phutil_get_library_root('phabricator'));
         require_once $root.'/externals/JsShrink/jsShrink.php';
         $data = jsShrink($data);
 
         break;
     }
 
     return $data;
   }
 
   public static function getResourceType($path) {
     return last(explode('.', $path));
   }
 
   public function translateResourceURI(array $matches) {
     $uri = trim($matches[1], "'\" \r\t\n");
     $tail = '';
 
     // If the resource URI has a query string or anchor, strip it off before
     // we go looking for the resource. We'll stitch it back on later. This
     // primarily affects FontAwesome.
 
     $parts = preg_split('/(?=[?#])/', $uri, 2);
     if (count($parts) == 2) {
       $uri = $parts[0];
       $tail = $parts[1];
     }
 
     $alternatives = array_unique(
       array(
         $uri,
         ltrim($uri, '/'),
       ));
 
     foreach ($alternatives as $alternative) {
       if ($this->rawURIMap !== null) {
         if (isset($this->rawURIMap[$alternative])) {
           $uri = $this->rawURIMap[$alternative];
           break;
         }
       }
 
       if ($this->celerityMap) {
         $resource_uri = $this->celerityMap->getURIForName($alternative);
         if ($resource_uri) {
           // Check if we can use a data URI for this resource. If not, just
           // use a normal Celerity URI.
           $data_uri = $this->generateDataURI($alternative);
           if ($data_uri) {
             $uri = $data_uri;
           } else {
             $uri = $resource_uri;
           }
           break;
         }
       }
     }
 
     return 'url('.$uri.$tail.')';
   }
 
   private function replaceCSSVariables($path, $data) {
     $this->currentPath = $path;
     return preg_replace_callback(
       '/{\$([^}]+)}/',
       array($this, 'replaceCSSVariable'),
       $data);
   }
 
   private function replaceCSSPrintRules($path, $data) {
     $this->currentPath = $path;
     return preg_replace_callback(
       '/!print\s+(.+?{.+?})/s',
       array($this, 'replaceCSSPrintRule'),
       $data);
   }
 
   public static function getCSSVariableMap() {
     return array(
       // Fonts
       'basefont' => "13px/1.231 'Lato', 'Segoe UI', 'Segoe UI Web Regular', ".
         "'Segoe UI Symbol', 'Helvetica Neue', Helvetica, Arial, sans-serif",
 
       'fontfamily' => "'Lato', 'Segoe UI', 'Segoe UI Web Regular', ".
         "'Segoe UI Symbol', 'Helvetica Neue', Helvetica, Arial, sans-serif",
 
       // Drop Shadow
       'dropshadow' => '0 1px 6px rgba(0, 0, 0, .25)',
       'whitetextshadow' => '0 1px 0 rgba(255, 255, 255, 1)',
 
       // Anchors
       'anchor' => '#136CB2',
 
       // Base Colors
       'red'           => '#c0392b',
       'lightred'      => '#f4dddb',
       'orange'        => '#e67e22',
       'lightorange'   => '#f7e2d4',
       'yellow'        => '#f1c40f',
       'lightyellow'   => '#fdf5d4',
       'green'         => '#139543',
       'lightgreen'    => '#d7eddf',
       'blue'          => '#2980b9',
       'lightblue'     => '#daeaf3',
       'sky'           => '#3498db',
       'lightsky'      => '#ddeef9',
       'fire'          => '#e62f17',
       'indigo'        => '#6e5cb6',
       'lightindigo'   => '#eae6f7',
       'pink'          => '#da49be',
       'lightpink'     => '#fbeaf8',
       'violet'        => '#8e44ad',
       'lightviolet'   => '#ecdff1',
       'charcoal'      => '#4b4d51',
       'backdrop'      => '#dadee7',
       'hoverwhite'    => 'rgba(255,255,255,.75)',
       'hovergrey'     => '#c5cbcf',
       'hoverblue'     => '#eceff5',
       'hoverborder'   => '#dfe1e9',
       'hoverselectedgrey' => '#bbc4ca',
       'hoverselectedblue' => '#e6e9ee',
 
       // Base Greys
       'lightgreyborder'     => '#C7CCD9',
       'greyborder'          => '#A1A6B0',
       'darkgreyborder'      => '#676A70',
       'lightgreytext'       => '#92969D',
       'greytext'            => '#74777D',
       'darkgreytext'        => '#4B4D51',
       'lightgreybackground' => '#F7F7F7',
       'greybackground'      => '#EBECEE',
       'darkgreybackground'  => '#DFE0E2',
 
       // Base Blues
       'thinblueborder'      => '#DDE8EF',
       'lightblueborder'     => '#BFCFDA',
       'blueborder'          => '#8C98B8',
       'darkblueborder'      => '#626E82',
       'lightbluebackground' => '#F8F9FC',
       'bluebackground'      => '#DAE7FF',
       'lightbluetext'       => '#8C98B8',
       'bluetext'            => '#6B748C',
       'darkbluetext'        => '#464C5C',
 
       // Base Greens
       'lightgreenborder'      => '#bfdac1',
       'greenborder'           => '#8cb89c',
       'greentext'             => '#3e6d35',
       'lightgreenbackground'  => '#e6f2e4',
 
       // Base Red
       'lightredborder'        => '#f4c6c6',
       'redborder'             => '#eb9797',
       'redtext'               => '#802b2b',
       'lightredbackground'    => '#f5e1e1',
 
       // Base Violet
       'lightvioletborder'     => '#cfbddb',
       'violetborder'          => '#b589ba',
       'violettext'            => '#603c73',
       'lightvioletbackground' => '#e9dfee',
 
       // Shades are a more muted set of our base colors
       // better suited to blending into other UIs.
 
       // Shade Red
       'sh-lightredborder'     => '#efcfcf',
       'sh-redborder'          => '#d1abab',
       'sh-redicon'            => '#c85a5a',
       'sh-redtext'            => '#a53737',
       'sh-redbackground'      => '#f7e6e6',
 
       // Shade Orange
       'sh-lightorangeborder'  => '#f8dcc3',
       'sh-orangeborder'       => '#dbb99e',
       'sh-orangeicon'         => '#e78331',
       'sh-orangetext'         => '#ba6016',
       'sh-orangebackground'   => '#fbede1',
 
       // Shade Yellow
       'sh-lightyellowborder'  => '#e9dbcd',
       'sh-yellowborder'       => '#c9b8a8',
       'sh-yellowicon'         => '#9b946e',
       'sh-yellowtext'         => '#726f56',
       'sh-yellowbackground'   => '#fdf3da',
 
       // Shade Green
       'sh-lightgreenborder'   => '#c6e6c7',
       'sh-greenborder'        => '#a0c4a1',
       'sh-greenicon'          => '#4ca74e',
       'sh-greentext'          => '#326d34',
       'sh-greenbackground'    => '#ddefdd',
 
       // Shade Blue
       'sh-lightblueborder'    => '#cfdbe3',
       'sh-blueborder'         => '#a7b5bf',
       'sh-blueicon'           => '#6b748c',
       'sh-bluetext'           => '#464c5c',
       'sh-bluebackground'     => '#dee7f8',
 
       // Shade Indigo
       'sh-lightindigoborder'  => '#d1c9ee',
       'sh-indigoborder'       => '#bcb4da',
       'sh-indigoicon'         => '#8672d4',
       'sh-indigotext'         => '#6e5cb6',
       'sh-indigobackground'   => '#eae6f7',
 
       // Shade Violet
       'sh-lightvioletborder'  => '#e0d1e7',
       'sh-violetborder'       => '#bcabc5',
       'sh-violeticon'         => '#9260ad',
       'sh-violettext'         => '#69427f',
       'sh-violetbackground'   => '#efe8f3',
 
       // Shade Pink
       'sh-lightpinkborder'  => '#f6d5ef',
       'sh-pinkborder'       => '#d5aecd',
       'sh-pinkicon'         => '#e26fcb',
       'sh-pinktext'         => '#da49be',
       'sh-pinkbackground'   => '#fbeaf8',
 
       // Shade Grey
       'sh-lightgreyborder'    => '#d8d8d8',
       'sh-greyborder'         => '#b2b2b2',
       'sh-greyicon'           => '#757575',
       'sh-greytext'           => '#555555',
       'sh-greybackground'     => '#e7e7e7',
 
       // Shade Disabled
       'sh-lightdisabledborder'  => '#e5e5e5',
       'sh-disabledborder'       => '#cbcbcb',
       'sh-disabledicon'         => '#bababa',
       'sh-disabledtext'         => '#a6a6a6',
       'sh-disabledbackground'   => '#f3f3f3',
 
     );
   }
 
 
   public function replaceCSSVariable($matches) {
     static $map;
     if (!$map) {
       $map = self::getCSSVariableMap();
     }
 
     $var_name = $matches[1];
     if (empty($map[$var_name])) {
       $path = $this->currentPath;
       throw new Exception(
         pht(
           "CSS file '%s' has unknown variable '%s'.",
           $path,
           $var_name));
     }
 
     return $map[$var_name];
   }
 
   public function replaceCSSPrintRule($matches) {
     $rule = $matches[1];
 
     $rules = array();
     $rules[] = '.printable '.$rule;
     $rules[] = "@media print {\n  ".str_replace("\n", "\n  ", $rule)."\n}\n";
 
     return implode("\n\n", $rules);
   }
 
 
   /**
    * Attempt to generate a data URI for a resource. We'll generate a data URI
    * if the resource is a valid resource of an appropriate type, and is
    * small enough. Otherwise, this method will return `null` and we'll end up
    * using a normal URI instead.
    *
    * @param string  Resource name to attempt to generate a data URI for.
    * @return string|null Data URI, or null if we declined to generate one.
    */
   private function generateDataURI($resource_name) {
     $ext = last(explode('.', $resource_name));
     switch ($ext) {
       case 'png':
         $type = 'image/png';
         break;
       case 'gif':
         $type = 'image/gif';
         break;
       case 'jpg':
         $type = 'image/jpeg';
         break;
       default:
         return null;
     }
 
     // In IE8, 32KB is the maximum supported URI length.
     $maximum_data_size = (1024 * 32);
 
     $data = $this->celerityMap->getResourceDataForName($resource_name);
     if (strlen($data) >= $maximum_data_size) {
       // If the data is already too large on its own, just bail before
       // encoding it.
       return null;
     }
 
     $uri = 'data:'.$type.';base64,'.base64_encode($data);
     if (strlen($uri) >= $maximum_data_size) {
       return null;
     }
 
     return $uri;
   }
 
 }
diff --git a/src/applications/celerity/CeleritySpriteGenerator.php b/src/applications/celerity/CeleritySpriteGenerator.php
index 13936f205..48422da57 100644
--- a/src/applications/celerity/CeleritySpriteGenerator.php
+++ b/src/applications/celerity/CeleritySpriteGenerator.php
@@ -1,263 +1,263 @@
 <?php
 
-final class CeleritySpriteGenerator {
+final class CeleritySpriteGenerator extends Phobject {
 
   public function buildMenuSheet() {
     $sprites = array();
 
     $colors = array(
       'dark',
       'light',
     );
 
     $sources = array();
     foreach ($colors as $color) {
       $sources[$color.'-logo'] = array(
         'x' => 96,
         'y' => 40,
         'css' => '.'.$color.'-logo',
       );
       $sources[$color.'-eye'] = array(
         'x' => 40,
         'y' => 40,
         'css' => '.'.$color.'-eye',
       );
     }
 
     $scales = array(
       '1x' => 1,
       '2x' => 2,
     );
 
     $template = new PhutilSprite();
     foreach ($sources as $name => $spec) {
       $sprite = id(clone $template)
         ->setName($name)
         ->setSourceSize($spec['x'], $spec['y'])
         ->setTargetCSS($spec['css']);
 
       foreach ($scales as $scale_name => $scale) {
         $path = 'menu_'.$scale_name.'/'.$name.'.png';
         $path = $this->getPath($path);
 
         $sprite->setSourceFile($path, $scale);
       }
       $sprites[] = $sprite;
     }
 
     $sheet = $this->buildSheet('menu', true);
     $sheet->setScales($scales);
     foreach ($sprites as $sprite) {
       $sheet->addSprite($sprite);
     }
 
     return $sheet;
   }
 
   public function buildTokenSheet() {
     $icons = $this->getDirectoryList('tokens_1x');
     $scales = array(
       '1x' => 1,
       '2x' => 2,
     );
     $template = id(new PhutilSprite())
       ->setSourceSize(16, 16);
 
     $sprites = array();
     $prefix = 'tokens_';
     foreach ($icons as $icon) {
       $sprite = id(clone $template)
         ->setName('tokens-'.$icon)
         ->setTargetCSS('.tokens-'.$icon);
 
       foreach ($scales as $scale_key => $scale) {
         $path = $this->getPath($prefix.$scale_key.'/'.$icon.'.png');
         $sprite->setSourceFile($path, $scale);
       }
       $sprites[] = $sprite;
     }
 
     $sheet = $this->buildSheet('tokens', true);
     $sheet->setScales($scales);
     foreach ($sprites as $sprite) {
       $sheet->addSprite($sprite);
     }
 
     return $sheet;
   }
 
   public function buildProjectsSheet() {
     $icons = $this->getDirectoryList('projects_1x');
     $scales = array(
       '1x' => 1,
       '2x' => 2,
     );
     $template = id(new PhutilSprite())
       ->setSourceSize(50, 50);
 
     $sprites = array();
     $prefix = 'projects-';
     foreach ($icons as $icon) {
       $sprite = id(clone $template)
         ->setName($prefix.$icon)
         ->setTargetCSS('.'.$prefix.$icon);
 
       foreach ($scales as $scale_key => $scale) {
         $path = $this->getPath('projects_'.$scale_key.'/'.$icon.'.png');
         $sprite->setSourceFile($path, $scale);
       }
       $sprites[] = $sprite;
     }
 
     $sheet = $this->buildSheet('projects', true);
     $sheet->setScales($scales);
     foreach ($sprites as $sprite) {
       $sheet->addSprite($sprite);
     }
 
     return $sheet;
   }
 
   public function buildLoginSheet() {
     $icons = $this->getDirectoryList('login_1x');
     $scales = array(
       '1x' => 1,
       '2x' => 2,
     );
     $template = id(new PhutilSprite())
       ->setSourceSize(34, 34);
 
     $sprites = array();
     $prefix = 'login_';
     foreach ($icons as $icon) {
       $sprite = id(clone $template)
         ->setName('login-'.$icon)
         ->setTargetCSS('.login-'.$icon);
 
       foreach ($scales as $scale_key => $scale) {
         $path = $this->getPath($prefix.$scale_key.'/'.$icon.'.png');
         $sprite->setSourceFile($path, $scale);
       }
       $sprites[] = $sprite;
     }
 
     $sheet = $this->buildSheet('login', true);
     $sheet->setScales($scales);
     foreach ($sprites as $sprite) {
       $sheet->addSprite($sprite);
     }
 
     return $sheet;
   }
 
   public function buildMainHeaderSheet() {
     $gradients = $this->getDirectoryList('main_header');
     $template = new PhutilSprite();
 
     $sprites = array();
     foreach ($gradients as $gradient) {
       $path = $this->getPath('main_header/'.$gradient.'.png');
       $sprite = id(clone $template)
         ->setName('main-header-'.$gradient)
         ->setSourceFile($path)
         ->setTargetCSS('.phui-theme-'.$gradient.
           ' .phabricator-main-menu-background');
       $sprite->setSourceSize(6, 44);
       $sprites[] = $sprite;
     }
 
     $sheet = $this->buildSheet('main-header',
       false,
       PhutilSpriteSheet::TYPE_REPEAT_X);
 
     foreach ($sprites as $sprite) {
       $sheet->addSprite($sprite);
     }
 
     return $sheet;
   }
 
   private function getPath($to_path = null) {
     $root = dirname(phutil_get_library_root('phabricator'));
     return $root.'/resources/sprite/'.$to_path;
   }
 
   private function getDirectoryList($dir) {
     $path = $this->getPath($dir);
 
     $result = array();
 
     $images = Filesystem::listDirectory($path, $include_hidden = false);
     foreach ($images as $image) {
       if (!preg_match('/\.png$/', $image)) {
         throw new Exception(
           pht(
             "Expected file '%s' in '%s' to be a sprite source ending in '%s'.",
             $image,
             $path,
             '.png'));
       }
       $result[] = substr($image, 0, -4);
     }
 
     return $result;
   }
 
   private function buildSheet(
     $name,
     $has_retina,
     $type = null,
     $extra_css = '') {
 
     $sheet = new PhutilSpriteSheet();
 
     $at = '@';
 
     switch ($type) {
       case PhutilSpriteSheet::TYPE_STANDARD:
       default:
         $type = PhutilSpriteSheet::TYPE_STANDARD;
         $repeat_rule = 'no-repeat';
         break;
       case PhutilSpriteSheet::TYPE_REPEAT_X:
         $repeat_rule = 'repeat-x';
         break;
       case PhutilSpriteSheet::TYPE_REPEAT_Y:
         $repeat_rule = 'repeat-y';
         break;
     }
 
     $retina_rules = null;
     if ($has_retina) {
       $retina_rules = <<<EOCSS
 @media
 only screen and (min-device-pixel-ratio: 1.5),
 only screen and (-webkit-min-device-pixel-ratio: 1.5) {
   .sprite-{$name}{$extra_css} {
     background-image: url(/rsrc/image/sprite-{$name}-X2.png);
     background-size: {X}px {Y}px;
   }
 }
 EOCSS;
     }
 
     $sheet->setSheetType($type);
     $sheet->setCSSHeader(<<<EOCSS
 /**
  * @provides sprite-{$name}-css
  * {$at}generated
  */
 
 .sprite-{$name}{$extra_css} {
   background-image: url(/rsrc/image/sprite-{$name}.png);
   background-repeat: {$repeat_rule};
 }
 
 {$retina_rules}
 
 EOCSS
 );
 
     return $sheet;
   }
 }
diff --git a/src/applications/celerity/CelerityStaticResourceResponse.php b/src/applications/celerity/CelerityStaticResourceResponse.php
index cfb586602..60b1d6989 100644
--- a/src/applications/celerity/CelerityStaticResourceResponse.php
+++ b/src/applications/celerity/CelerityStaticResourceResponse.php
@@ -1,321 +1,321 @@
 <?php
 
 /**
  * Tracks and resolves dependencies the page declares with
  * @{function:require_celerity_resource}, and then builds appropriate HTML or
  * Ajax responses.
  */
-final class CelerityStaticResourceResponse {
+final class CelerityStaticResourceResponse extends Phobject {
 
   private $symbols = array();
   private $needsResolve = true;
   private $resolved;
   private $packaged;
   private $metadata = array();
   private $metadataBlock = 0;
   private $behaviors = array();
   private $hasRendered = array();
 
   public function __construct() {
     if (isset($_REQUEST['__metablock__'])) {
       $this->metadataBlock = (int)$_REQUEST['__metablock__'];
     }
   }
 
   public function addMetadata($metadata) {
     $id = count($this->metadata);
     $this->metadata[$id] = $metadata;
     return $this->metadataBlock.'_'.$id;
   }
 
   public function getMetadataBlock() {
     return $this->metadataBlock;
   }
 
   /**
    * Register a behavior for initialization.
    *
    * NOTE: If `$config` is empty, a behavior will execute only once even if it
    * is initialized multiple times. If `$config` is nonempty, the behavior will
    * be invoked once for each configuration.
    */
   public function initBehavior(
     $behavior,
     array $config = array(),
     $source_name = null) {
 
     $this->requireResource('javelin-behavior-'.$behavior, $source_name);
 
     if (empty($this->behaviors[$behavior])) {
       $this->behaviors[$behavior] = array();
     }
 
     if ($config) {
       $this->behaviors[$behavior][] = $config;
     }
 
     return $this;
   }
 
   public function requireResource($symbol, $source_name) {
     if (isset($this->symbols[$source_name][$symbol])) {
       return $this;
     }
 
     // Verify that the resource exists.
     $map = CelerityResourceMap::getNamedInstance($source_name);
     $name = $map->getResourceNameForSymbol($symbol);
     if ($name === null) {
       throw new Exception(
         pht(
           'No resource with symbol "%s" exists in source "%s"!',
           $symbol,
           $source_name));
     }
 
     $this->symbols[$source_name][$symbol] = true;
     $this->needsResolve = true;
 
     return $this;
   }
 
   private function resolveResources() {
     if ($this->needsResolve) {
       $this->packaged = array();
       foreach ($this->symbols as $source_name => $symbols_map) {
         $symbols = array_keys($symbols_map);
 
         $map = CelerityResourceMap::getNamedInstance($source_name);
         $packaged = $map->getPackagedNamesForSymbols($symbols);
 
         $this->packaged[$source_name] = $packaged;
       }
       $this->needsResolve = false;
     }
     return $this;
   }
 
   public function renderSingleResource($symbol, $source_name) {
     $map = CelerityResourceMap::getNamedInstance($source_name);
     $packaged = $map->getPackagedNamesForSymbols(array($symbol));
     return $this->renderPackagedResources($map, $packaged);
   }
 
   public function renderResourcesOfType($type) {
     $this->resolveResources();
 
     $result = array();
     foreach ($this->packaged as $source_name => $resource_names) {
       $map = CelerityResourceMap::getNamedInstance($source_name);
 
       $resources_of_type = array();
       foreach ($resource_names as $resource_name) {
         $resource_type = $map->getResourceTypeForName($resource_name);
         if ($resource_type == $type) {
           $resources_of_type[] = $resource_name;
         }
       }
 
       $result[] = $this->renderPackagedResources($map, $resources_of_type);
     }
 
     return phutil_implode_html('', $result);
   }
 
   private function renderPackagedResources(
     CelerityResourceMap $map,
     array $resources) {
 
     $output = array();
     foreach ($resources as $name) {
       if (isset($this->hasRendered[$name])) {
         continue;
       }
       $this->hasRendered[$name] = true;
 
       $output[] = $this->renderResource($map, $name);
     }
 
     return $output;
   }
 
   private function renderResource(
     CelerityResourceMap $map,
     $name) {
 
     $uri = $this->getURI($map, $name);
     $type = $map->getResourceTypeForName($name);
 
     $multimeter = MultimeterControl::getInstance();
     if ($multimeter) {
       $event_type = MultimeterEvent::TYPE_STATIC_RESOURCE;
       $multimeter->newEvent($event_type, 'rsrc.'.$name, 1);
     }
 
     switch ($type) {
       case 'css':
         return phutil_tag(
           'link',
           array(
             'rel'   => 'stylesheet',
             'type'  => 'text/css',
             'href'  => $uri,
           ));
       case 'js':
         return phutil_tag(
           'script',
           array(
             'type'  => 'text/javascript',
             'src'   => $uri,
           ),
           '');
     }
 
     throw new Exception(
       pht(
         'Unable to render resource "%s", which has unknown type "%s".',
         $name,
         $type));
   }
 
   public function renderHTMLFooter() {
     $data = array();
     if ($this->metadata) {
       $json_metadata = AphrontResponse::encodeJSONForHTTPResponse(
         $this->metadata);
       $this->metadata = array();
     } else {
       $json_metadata = '{}';
     }
     // Even if there is no metadata on the page, Javelin uses the mergeData()
     // call to start dispatching the event queue.
     $data[] = 'JX.Stratcom.mergeData('.$this->metadataBlock.', '.
                                        $json_metadata.');';
 
     $onload = array();
     if ($this->behaviors) {
       $behaviors = $this->behaviors;
       $this->behaviors = array();
 
       $higher_priority_names = array(
         'refresh-csrf',
         'aphront-basic-tokenizer',
         'dark-console',
         'history-install',
       );
 
       $higher_priority_behaviors = array_select_keys(
         $behaviors,
         $higher_priority_names);
 
       foreach ($higher_priority_names as $name) {
         unset($behaviors[$name]);
       }
 
       $behavior_groups = array(
         $higher_priority_behaviors,
         $behaviors,
       );
 
       foreach ($behavior_groups as $group) {
         if (!$group) {
           continue;
         }
         $group_json = AphrontResponse::encodeJSONForHTTPResponse(
           $group);
         $onload[] = 'JX.initBehaviors('.$group_json.')';
       }
     }
 
     if ($onload) {
       foreach ($onload as $func) {
         $data[] = 'JX.onload(function(){'.$func.'});';
       }
     }
 
     if ($data) {
       $data = implode("\n", $data);
       return self::renderInlineScript($data);
     } else {
       return '';
     }
   }
 
   public static function renderInlineScript($data) {
     if (stripos($data, '</script>') !== false) {
       throw new Exception(
         pht(
           'Literal %s is not allowed inside inline script.',
           '</script>'));
     }
     if (strpos($data, '<!') !== false) {
       throw new Exception(
         pht(
           'Literal %s is not allowed inside inline script.',
           '<!'));
     }
     // We don't use <![CDATA[ ]]> because it is ignored by HTML parsers. We
     // would need to send the document with XHTML content type.
     return phutil_tag(
       'script',
       array('type' => 'text/javascript'),
       phutil_safe_html($data));
   }
 
   public function buildAjaxResponse($payload, $error = null) {
     $response = array(
       'error'   => $error,
       'payload' => $payload,
     );
 
     if ($this->metadata) {
       $response['javelin_metadata'] = $this->metadata;
       $this->metadata = array();
     }
 
     if ($this->behaviors) {
       $response['javelin_behaviors'] = $this->behaviors;
       $this->behaviors = array();
     }
 
     $this->resolveResources();
     $resources = array();
     foreach ($this->packaged as $source_name => $resource_names) {
       $map = CelerityResourceMap::getNamedInstance($source_name);
       foreach ($resource_names as $resource_name) {
         $resources[] = $this->getURI($map, $resource_name);
       }
     }
     if ($resources) {
       $response['javelin_resources'] = $resources;
     }
 
     return $response;
   }
 
   public function getURI(
     CelerityResourceMap $map,
     $name,
     $use_primary_domain = false) {
 
     $uri = $map->getURIForName($name);
 
     // In developer mode, we dump file modification times into the URI. When a
     // page is reloaded in the browser, any resources brought in by Ajax calls
     // do not trigger revalidation, so without this it's very difficult to get
     // changes to Ajaxed-in CSS to work (you must clear your cache or rerun
     // the map script). In production, we can assume the map script gets run
     // after changes, and safely skip this.
     if (PhabricatorEnv::getEnvConfig('phabricator.developer-mode')) {
       $mtime = $map->getModifiedTimeForName($name);
       $uri = preg_replace('@^/res/@', '/res/'.$mtime.'T/', $uri);
     }
 
     if ($use_primary_domain) {
       return PhabricatorEnv::getURI($uri);
     } else {
       return PhabricatorEnv::getCDNURI($uri);
     }
   }
 
 }
diff --git a/src/applications/celerity/resources/CelerityPhysicalResources.php b/src/applications/celerity/resources/CelerityPhysicalResources.php
index 41665d15f..730c8dc43 100644
--- a/src/applications/celerity/resources/CelerityPhysicalResources.php
+++ b/src/applications/celerity/resources/CelerityPhysicalResources.php
@@ -1,61 +1,62 @@
 <?php
 
 /**
  * Defines the location of physical static resources which exist at build time
  * and are precomputed into a resource map.
  */
 abstract class CelerityPhysicalResources extends CelerityResources {
 
   private $map;
 
   abstract public function getPathToMap();
   abstract public function findBinaryResources();
   abstract public function findTextResources();
 
   public function loadMap() {
     if ($this->map === null) {
       $this->map = include $this->getPathToMap();
     }
     return $this->map;
   }
 
   public static function getAll() {
     static $resources_map;
+
     if ($resources_map === null) {
       $resources_map = array();
 
       $resources_list = id(new PhutilSymbolLoader())
         ->setAncestorClass(__CLASS__)
         ->loadObjects();
 
       foreach ($resources_list as $resources) {
         $name = $resources->getName();
 
         if (!preg_match('/^[a-z0-9]+/', $name)) {
           throw new Exception(
             pht(
               'Resources name "%s" is not valid; it must contain only '.
               'lowercase latin letters and digits.',
               $name));
         }
 
         if (empty($resources_map[$name])) {
           $resources_map[$name] = $resources;
         } else {
           $old = get_class($resources_map[$name]);
           $new = get_class($resources);
           throw new Exception(
             pht(
               'Celerity resource maps must have unique names, but maps %s and '.
               '%s share the same name, "%s".',
               $old,
               $new,
               $name));
         }
       }
     }
 
     return $resources_map;
   }
 
 }
diff --git a/src/applications/celerity/resources/CelerityResources.php b/src/applications/celerity/resources/CelerityResources.php
index a9604d944..b7c5cc248 100644
--- a/src/applications/celerity/resources/CelerityResources.php
+++ b/src/applications/celerity/resources/CelerityResources.php
@@ -1,40 +1,40 @@
 <?php
 
 /**
  * Defines the location of static resources.
  */
-abstract class CelerityResources {
+abstract class CelerityResources extends Phobject {
 
   private $map;
 
   abstract public function getName();
   abstract public function getResourceData($name);
 
   public function getResourceModifiedTime($name) {
     return 0;
   }
 
   public function getCelerityHash($data) {
     $tail = PhabricatorEnv::getEnvConfig('celerity.resource-hash');
     $hash = PhabricatorHash::digest($data, $tail);
     return substr($hash, 0, 8);
   }
 
   public function getResourceType($path) {
     return CelerityResourceTransformer::getResourceType($path);
   }
 
   public function getResourceURI($hash, $name) {
     $resources = $this->getName();
     return "/res/{$resources}/{$hash}/{$name}";
   }
 
   public function getResourcePackages() {
     return array();
   }
 
   public function loadMap() {
     return array();
   }
 
 }
diff --git a/src/applications/celerity/resources/__tests__/CelerityPhysicalResourcesTestCase.php b/src/applications/celerity/resources/__tests__/CelerityPhysicalResourcesTestCase.php
new file mode 100644
index 000000000..a7ac29649
--- /dev/null
+++ b/src/applications/celerity/resources/__tests__/CelerityPhysicalResourcesTestCase.php
@@ -0,0 +1,10 @@
+<?php
+
+final class CelerityPhysicalResourcesTestCase extends PhabricatorTestCase {
+
+  public function testGetAll() {
+    CelerityPhysicalResources::getAll();
+    $this->assertTrue(true);
+  }
+
+}
diff --git a/src/applications/conduit/call/ConduitCall.php b/src/applications/conduit/call/ConduitCall.php
index 7d767cd8c..89bddad54 100644
--- a/src/applications/conduit/call/ConduitCall.php
+++ b/src/applications/conduit/call/ConduitCall.php
@@ -1,158 +1,159 @@
 <?php
 
 /**
  * Run a conduit method in-process, without requiring HTTP requests. Usage:
  *
  *   $call = new ConduitCall('method.name', array('param' => 'value'));
  *   $call->setUser($user);
  *   $result = $call->execute();
  *
  */
-final class ConduitCall {
+final class ConduitCall extends Phobject {
 
   private $method;
+  private $handler;
   private $request;
   private $user;
 
   public function __construct($method, array $params) {
     $this->method = $method;
     $this->handler = $this->buildMethodHandler($method);
 
     $param_types = $this->handler->getParamTypes();
 
     foreach ($param_types as $key => $spec) {
       if (ConduitAPIMethod::getParameterMetadataKey($key) !== null) {
         throw new ConduitException(
           pht(
             'API Method "%s" defines a disallowed parameter, "%s". This '.
             'parameter name is reserved.',
             $method,
             $key));
       }
     }
 
     $invalid_params = array_diff_key($params, $param_types);
     if ($invalid_params) {
       throw new ConduitException(
         pht(
           'API Method "%s" does not define these parameters: %s.',
           $method,
           "'".implode("', '", array_keys($invalid_params))."'"));
     }
 
     $this->request = new ConduitAPIRequest($params);
   }
 
   public function getAPIRequest() {
     return $this->request;
   }
 
   public function setUser(PhabricatorUser $user) {
     $this->user = $user;
     return $this;
   }
 
   public function getUser() {
     return $this->user;
   }
 
   public function shouldRequireAuthentication() {
     return $this->handler->shouldRequireAuthentication();
   }
 
   public function shouldAllowUnguardedWrites() {
     return $this->handler->shouldAllowUnguardedWrites();
   }
 
   public function getRequiredScope() {
     return $this->handler->getRequiredScope();
   }
 
   public function getErrorDescription($code) {
     return $this->handler->getErrorDescription($code);
   }
 
   public function execute() {
     $profiler = PhutilServiceProfiler::getInstance();
     $call_id = $profiler->beginServiceCall(
       array(
         'type' => 'conduit',
         'method' => $this->method,
       ));
 
     try {
       $result = $this->executeMethod();
     } catch (Exception $ex) {
       $profiler->endServiceCall($call_id, array());
       throw $ex;
     }
 
     $profiler->endServiceCall($call_id, array());
     return $result;
   }
 
   private function executeMethod() {
     $user = $this->getUser();
     if (!$user) {
       $user = new PhabricatorUser();
     }
 
     $this->request->setUser($user);
 
     if (!$this->shouldRequireAuthentication()) {
       // No auth requirement here.
     } else {
 
       $allow_public = $this->handler->shouldAllowPublic() &&
                       PhabricatorEnv::getEnvConfig('policy.allow-public');
       if (!$allow_public) {
         if (!$user->isLoggedIn() && !$user->isOmnipotent()) {
           // TODO: As per below, this should get centralized and cleaned up.
           throw new ConduitException('ERR-INVALID-AUTH');
         }
       }
 
       // TODO: This would be slightly cleaner by just using a Query, but the
       // Conduit auth workflow requires the Call and User be built separately.
       // Just do it this way for the moment.
       $application = $this->handler->getApplication();
       if ($application) {
         $can_view = PhabricatorPolicyFilter::hasCapability(
           $user,
           $application,
           PhabricatorPolicyCapability::CAN_VIEW);
 
         if (!$can_view) {
           throw new ConduitException(
             pht(
               'You do not have access to the application which provides this '.
               'API method.'));
         }
       }
     }
 
     return $this->handler->executeMethod($this->request);
   }
 
   protected function buildMethodHandler($method_name) {
     $method = ConduitAPIMethod::getConduitMethod($method_name);
 
     if (!$method) {
       throw new ConduitMethodDoesNotExistException($method_name);
     }
 
     $application = $method->getApplication();
     if ($application && !$application->isInstalled()) {
       $app_name = $application->getName();
       throw new ConduitApplicationNotInstalledException($method, $app_name);
     }
 
     return $method;
   }
 
   public function getMethodImplementation() {
     return $this->handler;
   }
 
 
 }
diff --git a/src/applications/conduit/method/ConduitAPIMethod.php b/src/applications/conduit/method/ConduitAPIMethod.php
index 8b9a67f4b..a56f68043 100644
--- a/src/applications/conduit/method/ConduitAPIMethod.php
+++ b/src/applications/conduit/method/ConduitAPIMethod.php
@@ -1,378 +1,383 @@
 <?php
 
 /**
  * @task  status  Method Status
  * @task  pager   Paging Results
  */
 abstract class ConduitAPIMethod
   extends Phobject
   implements PhabricatorPolicyInterface {
 
   const METHOD_STATUS_STABLE      = 'stable';
   const METHOD_STATUS_UNSTABLE    = 'unstable';
   const METHOD_STATUS_DEPRECATED  = 'deprecated';
 
   abstract public function getMethodDescription();
   abstract protected function defineParamTypes();
   abstract protected function defineReturnType();
 
   protected function defineErrorTypes() {
     return array();
   }
 
   abstract protected function execute(ConduitAPIRequest $request);
 
 
   public function __construct() {}
 
   public function getParamTypes() {
     $types = $this->defineParamTypes();
 
     $query = $this->newQueryObject();
     if ($query) {
       $types['order'] = 'order';
       $types += $this->getPagerParamTypes();
     }
 
     return $types;
   }
 
   public function getReturnType() {
     return $this->defineReturnType();
   }
 
   public function getErrorTypes() {
     return $this->defineErrorTypes();
   }
 
   /**
    * This is mostly for compatibility with
    * @{class:PhabricatorCursorPagedPolicyAwareQuery}.
    */
   public function getID() {
     return $this->getAPIMethodName();
   }
 
   /**
    * Get the status for this method (e.g., stable, unstable or deprecated).
    * Should return a METHOD_STATUS_* constant. By default, methods are
    * "stable".
    *
    * @return const  METHOD_STATUS_* constant.
    * @task status
    */
   public function getMethodStatus() {
     return self::METHOD_STATUS_STABLE;
   }
 
   /**
    * Optional description to supplement the method status. In particular, if
    * a method is deprecated, you can return a string here describing the reason
    * for deprecation and stable alternatives.
    *
    * @return string|null  Description of the method status, if available.
    * @task status
    */
   public function getMethodStatusDescription() {
     return null;
   }
 
   public function getErrorDescription($error_code) {
     return idx($this->getErrorTypes(), $error_code, pht('Unknown Error'));
   }
 
   public function getRequiredScope() {
     // by default, conduit methods are not accessible via OAuth
     return PhabricatorOAuthServerScope::SCOPE_NOT_ACCESSIBLE;
   }
 
   public function executeMethod(ConduitAPIRequest $request) {
     return $this->execute($request);
   }
 
   abstract public function getAPIMethodName();
 
   /**
    * Return a key which sorts methods by application name, then method status,
    * then method name.
    */
   public function getSortOrder() {
     $name = $this->getAPIMethodName();
 
     $map = array(
       self::METHOD_STATUS_STABLE      => 0,
       self::METHOD_STATUS_UNSTABLE    => 1,
       self::METHOD_STATUS_DEPRECATED  => 2,
     );
     $ord = idx($map, $this->getMethodStatus(), 0);
 
     list($head, $tail) = explode('.', $name, 2);
 
     return "{$head}.{$ord}.{$tail}";
   }
 
   public function getApplicationName() {
     return head(explode('.', $this->getAPIMethodName(), 2));
   }
 
-  public static function getConduitMethod($method_name) {
+  public static function loadAllConduitMethods() {
     static $method_map = null;
 
     if ($method_map === null) {
       $methods = id(new PhutilSymbolLoader())
         ->setAncestorClass(__CLASS__)
         ->loadObjects();
 
       foreach ($methods as $method) {
         $name = $method->getAPIMethodName();
 
         if (empty($method_map[$name])) {
           $method_map[$name] = $method;
           continue;
         }
 
         $orig_class = get_class($method_map[$name]);
         $this_class = get_class($method);
         throw new Exception(
           pht(
             'Two Conduit API method classes (%s, %s) both have the same '.
             'method name (%s). API methods must have unique method names.',
             $orig_class,
             $this_class,
             $name));
       }
     }
 
+    return $method_map;
+  }
+
+  public static function getConduitMethod($method_name) {
+    $method_map = self::loadAllConduitMethods();
     return idx($method_map, $method_name);
   }
 
   public function shouldRequireAuthentication() {
     return true;
   }
 
   public function shouldAllowPublic() {
     return false;
   }
 
   public function shouldAllowUnguardedWrites() {
     return false;
   }
 
 
   /**
    * Optionally, return a @{class:PhabricatorApplication} which this call is
    * part of. The call will be disabled when the application is uninstalled.
    *
    * @return PhabricatorApplication|null  Related application.
    */
   public function getApplication() {
     return null;
   }
 
   protected function formatStringConstants($constants) {
     foreach ($constants as $key => $value) {
       $constants[$key] = '"'.$value.'"';
     }
     $constants = implode(', ', $constants);
     return 'string-constant<'.$constants.'>';
   }
 
   public static function getParameterMetadataKey($key) {
     if (strncmp($key, 'api.', 4) === 0) {
       // All keys passed beginning with "api." are always metadata keys.
       return substr($key, 4);
     } else {
       switch ($key) {
         // These are real keys which always belong to request metadata.
         case 'access_token':
         case 'scope':
         case 'output':
 
         // This is not a real metadata key; it is included here only to
         // prevent Conduit methods from defining it.
         case '__conduit__':
 
         // This is prevented globally as a blanket defense against OAuth
         // redirection attacks. It is included here to stop Conduit methods
         // from defining it.
         case 'code':
 
         // This is not a real metadata key, but the presence of this
         // parameter triggers an alternate request decoding pathway.
         case 'params':
           return $key;
       }
     }
 
     return null;
   }
 
 /* -(  Paging Results  )----------------------------------------------------- */
 
 
   /**
    * @task pager
    */
   protected function getPagerParamTypes() {
     return array(
       'before' => 'optional string',
       'after'  => 'optional string',
       'limit'  => 'optional int (default = 100)',
     );
   }
 
 
   /**
    * @task pager
    */
   protected function newPager(ConduitAPIRequest $request) {
     $limit = $request->getValue('limit', 100);
     $limit = min(1000, $limit);
     $limit = max(1, $limit);
 
     $pager = id(new AphrontCursorPagerView())
       ->setPageSize($limit);
 
     $before_id = $request->getValue('before');
     if ($before_id !== null) {
       $pager->setBeforeID($before_id);
     }
 
     $after_id = $request->getValue('after');
     if ($after_id !== null) {
       $pager->setAfterID($after_id);
     }
 
     return $pager;
   }
 
 
   /**
    * @task pager
    */
   protected function addPagerResults(
     array $results,
     AphrontCursorPagerView $pager) {
 
     $results['cursor'] = array(
       'limit' => $pager->getPageSize(),
       'after' => $pager->getNextPageID(),
       'before' => $pager->getPrevPageID(),
     );
 
     return $results;
   }
 
 
 /* -(  Implementing Query Methods  )----------------------------------------- */
 
 
   public function newQueryObject() {
     return null;
   }
 
 
   protected function newQueryForRequest(ConduitAPIRequest $request) {
     $query = $this->newQueryObject();
 
     if (!$query) {
       throw new Exception(
         pht(
           'You can not call newQueryFromRequest() in this method ("%s") '.
           'because it does not implement newQueryObject().',
           get_class($this)));
     }
 
     if (!($query instanceof PhabricatorCursorPagedPolicyAwareQuery)) {
       throw new Exception(
         pht(
           'Call to method newQueryObject() did not return an object of class '.
           '"%s".',
           'PhabricatorCursorPagedPolicyAwareQuery'));
     }
 
     $query->setViewer($request->getUser());
 
     $order = $request->getValue('order');
     if ($order !== null) {
       if (is_scalar($order)) {
         $query->setOrder($order);
       } else {
         $query->setOrderVector($order);
       }
     }
 
     return $query;
   }
 
 
 /* -(  PhabricatorPolicyInterface  )----------------------------------------- */
 
 
   public function getPHID() {
     return null;
   }
 
   public function getCapabilities() {
     return array(
       PhabricatorPolicyCapability::CAN_VIEW,
     );
   }
 
   public function getPolicy($capability) {
     // Application methods get application visibility; other methods get open
     // visibility.
 
     $application = $this->getApplication();
     if ($application) {
       return $application->getPolicy($capability);
     }
 
     return PhabricatorPolicies::getMostOpenPolicy();
   }
 
   public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
     if (!$this->shouldRequireAuthentication()) {
       // Make unauthenticated methods universally visible.
       return true;
     }
 
     return false;
   }
 
   public function describeAutomaticCapability($capability) {
     return null;
   }
 
   protected function hasApplicationCapability(
     $capability,
     PhabricatorUser $viewer) {
 
     $application = $this->getApplication();
 
     if (!$application) {
       return false;
     }
 
     return PhabricatorPolicyFilter::hasCapability(
       $viewer,
       $application,
       $capability);
   }
 
   protected function requireApplicationCapability(
     $capability,
     PhabricatorUser $viewer) {
 
     $application = $this->getApplication();
     if (!$application) {
       return;
     }
 
     PhabricatorPolicyFilter::requireCapability(
       $viewer,
       $this->getApplication(),
       $capability);
   }
 
 }
diff --git a/src/applications/conduit/method/__tests__/ConduitAPIMethodTestCase.php b/src/applications/conduit/method/__tests__/ConduitAPIMethodTestCase.php
new file mode 100644
index 000000000..d0f54e297
--- /dev/null
+++ b/src/applications/conduit/method/__tests__/ConduitAPIMethodTestCase.php
@@ -0,0 +1,10 @@
+<?php
+
+final class ConduitAPIMethodTestCase extends PhabricatorTestCase {
+
+  public function testLoadAllConduitMethods() {
+    ConduitAPIMethod::loadAllConduitMethods();
+    $this->assertTrue(true);
+  }
+
+}
diff --git a/src/applications/conduit/protocol/ConduitAPIRequest.php b/src/applications/conduit/protocol/ConduitAPIRequest.php
index fe8af34b6..be1b88323 100644
--- a/src/applications/conduit/protocol/ConduitAPIRequest.php
+++ b/src/applications/conduit/protocol/ConduitAPIRequest.php
@@ -1,56 +1,56 @@
 <?php
 
-final class ConduitAPIRequest {
+final class ConduitAPIRequest extends Phobject {
 
   protected $params;
   private $user;
   private $isClusterRequest = false;
 
   public function __construct(array $params) {
     $this->params = $params;
   }
 
   public function getValue($key, $default = null) {
     return coalesce(idx($this->params, $key), $default);
   }
 
   public function getAllParameters() {
     return $this->params;
   }
 
   public function setUser(PhabricatorUser $user) {
     $this->user = $user;
     return $this;
   }
 
   /**
    * Retrieve the authentic identity of the user making the request. If a
    * method requires authentication (the default) the user object will always
    * be available. If a method does not require authentication (i.e., overrides
    * shouldRequireAuthentication() to return false) the user object will NEVER
    * be available.
    *
    * @return PhabricatorUser Authentic user, available ONLY if the method
    *                         requires authentication.
    */
   public function getUser() {
     if (!$this->user) {
       throw new Exception(
         pht(
           'You can not access the user inside the implementation of a Conduit '.
           'method which does not require authentication (as per %s).',
           'shouldRequireAuthentication()'));
     }
     return $this->user;
   }
 
   public function setIsClusterRequest($is_cluster_request) {
     $this->isClusterRequest = $is_cluster_request;
     return $this;
   }
 
   public function getIsClusterRequest() {
     return $this->isClusterRequest;
   }
 
 }
diff --git a/src/applications/conduit/protocol/ConduitAPIResponse.php b/src/applications/conduit/protocol/ConduitAPIResponse.php
index 6a86aa38b..5df606ac5 100644
--- a/src/applications/conduit/protocol/ConduitAPIResponse.php
+++ b/src/applications/conduit/protocol/ConduitAPIResponse.php
@@ -1,43 +1,43 @@
 <?php
 
-final class ConduitAPIResponse {
+final class ConduitAPIResponse extends Phobject {
 
   private $result;
   private $errorCode;
   private $errorInfo;
 
   public function setResult($result) {
     $this->result = $result;
     return $this;
   }
 
   public function getResult() {
     return $this->result;
   }
 
   public function setErrorCode($error_code) {
     $this->errorCode = $error_code;
     return $this;
   }
 
   public function getErrorCode() {
     return $this->errorCode;
   }
 
   public function setErrorInfo($error_info) {
     $this->errorInfo = $error_info;
     return $this;
   }
   public function getErrorInfo() {
     return $this->errorInfo;
   }
 
   public function toDictionary() {
     return array(
       'result'     => $this->getResult(),
       'error_code' => $this->getErrorCode(),
       'error_info' => $this->getErrorInfo(),
     );
   }
 
 }
diff --git a/src/applications/config/check/PhabricatorSetupCheck.php b/src/applications/config/check/PhabricatorSetupCheck.php
index beb52de4d..2ea2facd4 100644
--- a/src/applications/config/check/PhabricatorSetupCheck.php
+++ b/src/applications/config/check/PhabricatorSetupCheck.php
@@ -1,172 +1,176 @@
 <?php
 
-abstract class PhabricatorSetupCheck {
+abstract class PhabricatorSetupCheck extends Phobject {
 
   private $issues;
 
   abstract protected function executeChecks();
 
   const GROUP_OTHER       = 'other';
   const GROUP_MYSQL       = 'mysql';
   const GROUP_PHP         = 'php';
   const GROUP_IMPORTANT   = 'important';
 
   public function getExecutionOrder() {
     return 1;
   }
 
   final protected function newIssue($key) {
     $issue = id(new PhabricatorSetupIssue())
       ->setIssueKey($key);
     $this->issues[$key] = $issue;
 
     if ($this->getDefaultGroup()) {
       $issue->setGroup($this->getDefaultGroup());
     }
 
     return $issue;
   }
 
   final public function getIssues() {
     return $this->issues;
   }
 
   protected function addIssue(PhabricatorSetupIssue $issue) {
     $this->issues[$issue->getIssueKey()] = $issue;
     return $this;
   }
 
   public function getDefaultGroup() {
     return null;
   }
 
   final public function runSetupChecks() {
     $this->issues = array();
     $this->executeChecks();
   }
 
   final public static function getOpenSetupIssueKeys() {
     $cache = PhabricatorCaches::getSetupCache();
     return $cache->getKey('phabricator.setup.issue-keys');
   }
 
   final public static function setOpenSetupIssueKeys(array $keys) {
     $cache = PhabricatorCaches::getSetupCache();
     $cache->setKey('phabricator.setup.issue-keys', $keys);
   }
 
   final public static function getUnignoredIssueKeys(array $all_issues) {
     assert_instances_of($all_issues, 'PhabricatorSetupIssue');
     $keys = array();
     foreach ($all_issues as $issue) {
       if (!$issue->getIsIgnored()) {
         $keys[] = $issue->getIssueKey();
       }
     }
     return $keys;
   }
 
   final public static function getConfigNeedsRepair() {
     $cache = PhabricatorCaches::getSetupCache();
     return $cache->getKey('phabricator.setup.needs-repair');
   }
 
   final public static function setConfigNeedsRepair($needs_repair) {
     $cache = PhabricatorCaches::getSetupCache();
     $cache->setKey('phabricator.setup.needs-repair', $needs_repair);
   }
 
   final public static function deleteSetupCheckCache() {
     $cache = PhabricatorCaches::getSetupCache();
     $cache->deleteKeys(
       array(
         'phabricator.setup.needs-repair',
         'phabricator.setup.issue-keys',
       ));
   }
 
   final public static function willProcessRequest() {
     $issue_keys = self::getOpenSetupIssueKeys();
     if ($issue_keys === null) {
       $issues = self::runAllChecks();
       foreach ($issues as $issue) {
         if ($issue->getIsFatal()) {
           $view = id(new PhabricatorSetupIssueView())
             ->setIssue($issue);
           return id(new PhabricatorConfigResponse())
             ->setView($view);
         }
       }
       self::setOpenSetupIssueKeys(self::getUnignoredIssueKeys($issues));
     }
 
     // Try to repair configuration unless we have a clean bill of health on it.
     // We need to keep doing this on every page load until all the problems
     // are fixed, which is why it's separate from setup checks (which run
     // once per restart).
     $needs_repair = self::getConfigNeedsRepair();
     if ($needs_repair !== false) {
       $needs_repair = self::repairConfig();
       self::setConfigNeedsRepair($needs_repair);
     }
   }
 
-  final public static function runAllChecks() {
+  final public static function loadAllChecks() {
     $symbols = id(new PhutilSymbolLoader())
       ->setAncestorClass(__CLASS__)
       ->setConcreteOnly(true)
       ->selectAndLoadSymbols();
 
     $checks = array();
     foreach ($symbols as $symbol) {
       $checks[] = newv($symbol['name'], array());
     }
 
-    $checks = msort($checks, 'getExecutionOrder');
+    return msort($checks, 'getExecutionOrder');
+  }
+
+  final public static function runAllChecks() {
+    $checks = self::loadAllChecks();
 
     $issues = array();
     foreach ($checks as $check) {
       $check->runSetupChecks();
       foreach ($check->getIssues() as $key => $issue) {
         if (isset($issues[$key])) {
           throw new Exception(
             pht(
               "Two setup checks raised an issue with key '%s'!",
               $key));
         }
         $issues[$key] = $issue;
         if ($issue->getIsFatal()) {
           break 2;
         }
       }
     }
 
     $ignore_issues = PhabricatorEnv::getEnvConfig('config.ignore-issues');
     foreach ($ignore_issues as $ignorable => $derp) {
       if (isset($issues[$ignorable])) {
         $issues[$ignorable]->setIsIgnored(true);
       }
     }
 
     return $issues;
   }
 
   final public static function repairConfig() {
     $needs_repair = false;
 
     $options = PhabricatorApplicationConfigOptions::loadAllOptions();
     foreach ($options as $option) {
       try {
         $option->getGroup()->validateOption(
           $option,
           PhabricatorEnv::getEnvConfig($option->getKey()));
       } catch (PhabricatorConfigValidationException $ex) {
         PhabricatorEnv::repairConfig($option->getKey(), $option->getDefault());
         $needs_repair = true;
       }
     }
 
     return $needs_repair;
   }
 
 }
diff --git a/src/applications/config/check/__tests__/PhabricatorSetupCheckTestCase.php b/src/applications/config/check/__tests__/PhabricatorSetupCheckTestCase.php
new file mode 100644
index 000000000..256b69ee7
--- /dev/null
+++ b/src/applications/config/check/__tests__/PhabricatorSetupCheckTestCase.php
@@ -0,0 +1,10 @@
+<?php
+
+final class PhabricatorSetupCheckTestCase extends PhabricatorTestCase {
+
+  public function testLoadAllChecks() {
+    PhabricatorSetupCheck::loadAllChecks();
+    $this->assertTrue(true);
+  }
+
+}
diff --git a/src/applications/config/custom/PhabricatorConfigOptionType.php b/src/applications/config/custom/PhabricatorConfigOptionType.php
index c50315a14..3f588452c 100644
--- a/src/applications/config/custom/PhabricatorConfigOptionType.php
+++ b/src/applications/config/custom/PhabricatorConfigOptionType.php
@@ -1,47 +1,47 @@
 <?php
 
-abstract class PhabricatorConfigOptionType {
+abstract class PhabricatorConfigOptionType extends Phobject {
 
   public function validateOption(PhabricatorConfigOption $option, $value) {
     return;
   }
 
   public function readRequest(
     PhabricatorConfigOption $option,
     AphrontRequest $request) {
 
     $e_value = null;
     $errors = array();
     $storage_value = $request->getStr('value');
     $display_value = $request->getStr('value');
 
     return array($e_value, $errors, $storage_value, $display_value);
   }
 
   public function getDisplayValue(
     PhabricatorConfigOption $option,
     PhabricatorConfigEntry $entry,
     $value) {
 
     if (is_array($value)) {
       $json = new PhutilJSON();
       return $json->encodeFormatted($value);
     } else {
       return $value;
     }
 
   }
 
   public function renderControl(
     PhabricatorConfigOption $option,
     $display_value,
     $e_value) {
 
     return id(new AphrontFormTextControl())
       ->setName('value')
       ->setLabel(pht('Value'))
       ->setValue($display_value)
       ->setError($e_value);
   }
 
 }
diff --git a/src/applications/config/issue/PhabricatorSetupIssue.php b/src/applications/config/issue/PhabricatorSetupIssue.php
index f1415fec0..1c2ff0f99 100644
--- a/src/applications/config/issue/PhabricatorSetupIssue.php
+++ b/src/applications/config/issue/PhabricatorSetupIssue.php
@@ -1,193 +1,193 @@
 <?php
 
-final class PhabricatorSetupIssue {
+final class PhabricatorSetupIssue extends Phobject {
 
   private $issueKey;
   private $name;
   private $message;
   private $isFatal;
   private $summary;
   private $shortName;
   private $group;
 
   private $isIgnored = false;
   private $phpExtensions = array();
   private $phabricatorConfig = array();
   private $relatedPhabricatorConfig = array();
   private $phpConfig = array();
   private $commands = array();
   private $mysqlConfig = array();
   private $originalPHPConfigValues = array();
   private $links;
 
   public function addCommand($command) {
     $this->commands[] = $command;
     return $this;
   }
 
   public function getCommands() {
     return $this->commands;
   }
 
   public function setShortName($short_name) {
     $this->shortName = $short_name;
     return $this;
   }
 
   public function getShortName() {
     if ($this->shortName === null) {
       return $this->getName();
     }
     return $this->shortName;
   }
 
   public function setGroup($group) {
     $this->group = $group;
     return $this;
   }
 
   public function getGroup() {
     if ($this->group) {
       return $this->group;
     } else {
       return PhabricatorSetupCheck::GROUP_OTHER;
     }
   }
 
   public function setName($name) {
     $this->name = $name;
     return $this;
   }
 
   public function getName() {
     return $this->name;
   }
 
   public function setSummary($summary) {
     $this->summary = $summary;
     return $this;
   }
 
   public function getSummary() {
     if ($this->summary === null) {
       return $this->getMessage();
     }
     return $this->summary;
   }
 
   public function setIssueKey($issue_key) {
     $this->issueKey = $issue_key;
     return $this;
   }
 
   public function getIssueKey() {
     return $this->issueKey;
   }
 
   public function setIsFatal($is_fatal) {
     $this->isFatal = $is_fatal;
     return $this;
   }
 
   public function getIsFatal() {
     return $this->isFatal;
   }
 
   public function addPHPConfig($php_config) {
     $this->phpConfig[] = $php_config;
     return $this;
   }
 
   /**
    * Set an explicit value to display when showing the user PHP configuration
    * values.
    *
    * If Phabricator has changed a value by the time a config issue is raised,
    * you can provide the original value here so the UI makes sense. For example,
    * we alter `memory_limit` during startup, so if the original value is not
    * provided it will look like it is always set to `-1`.
    *
    * @param string PHP configuration option to provide a value for.
    * @param string Explicit value to show in the UI.
    * @return this
    */
   public function addPHPConfigOriginalValue($php_config, $value) {
     $this->originalPHPConfigValues[$php_config] = $value;
     return $this;
   }
 
   public function getPHPConfigOriginalValue($php_config, $default = null) {
     return idx($this->originalPHPConfigValues, $php_config, $default);
   }
 
   public function getPHPConfig() {
     return $this->phpConfig;
   }
 
   public function addMySQLConfig($mysql_config) {
     $this->mysqlConfig[] = $mysql_config;
     return $this;
   }
 
   public function getMySQLConfig() {
     return $this->mysqlConfig;
   }
 
   public function addPhabricatorConfig($phabricator_config) {
     $this->phabricatorConfig[] = $phabricator_config;
     return $this;
   }
 
   public function getPhabricatorConfig() {
     return $this->phabricatorConfig;
   }
 
   public function addRelatedPhabricatorConfig($phabricator_config) {
     $this->relatedPhabricatorConfig[] = $phabricator_config;
     return $this;
   }
 
   public function getRelatedPhabricatorConfig() {
     return $this->relatedPhabricatorConfig;
   }
 
   public function addPHPExtension($php_extension) {
     $this->phpExtensions[] = $php_extension;
     return $this;
   }
 
   public function getPHPExtensions() {
     return $this->phpExtensions;
   }
 
   public function setMessage($message) {
     $this->message = $message;
     return $this;
   }
 
   public function getMessage() {
     return $this->message;
   }
 
   public function setIsIgnored($is_ignored) {
     $this->isIgnored = $is_ignored;
     return $this;
   }
 
   public function getIsIgnored() {
     return $this->isIgnored;
   }
 
   public function addLink($href, $name) {
     $this->links[] = array(
       'href' => $href,
       'name' => $name,
     );
     return $this;
   }
 
   public function getLinks() {
     return $this->links;
   }
 
 }
diff --git a/src/applications/config/json/PhabricatorConfigJSON.php b/src/applications/config/json/PhabricatorConfigJSON.php
index 2e82d1297..699c604e4 100644
--- a/src/applications/config/json/PhabricatorConfigJSON.php
+++ b/src/applications/config/json/PhabricatorConfigJSON.php
@@ -1,27 +1,27 @@
 <?php
 
-final class PhabricatorConfigJSON {
+final class PhabricatorConfigJSON extends Phobject {
   /**
    * Properly format a JSON value.
    *
    * @param wild Any value, but should be a raw value, not a string of JSON.
    * @return string
    */
   public static function prettyPrintJSON($value) {
     // Check not only that it's an array, but that it's an "unnatural" array
     // meaning that the keys aren't 0 -> size_of_array.
     if (is_array($value) && array_keys($value) != range(0, count($value) - 1)) {
       $result = id(new PhutilJSON())->encodeFormatted($value);
     } else {
       $result = json_encode($value);
     }
 
     // For readability, unescape forward slashes. These are normally escaped
     // to prevent the string "</script>" from appearing in a JSON literal,
     // but it's irrelevant here and makes reading paths more difficult than
     // necessary.
     $result = str_replace('\\/', '/', $result);
     return $result;
 
   }
 }
diff --git a/src/applications/conpherence/ConpherenceTransactionRenderer.php b/src/applications/conpherence/ConpherenceTransactionRenderer.php
index 65bfacb3c..ff8bb5251 100644
--- a/src/applications/conpherence/ConpherenceTransactionRenderer.php
+++ b/src/applications/conpherence/ConpherenceTransactionRenderer.php
@@ -1,177 +1,177 @@
 <?php
 
-final class ConpherenceTransactionRenderer {
+final class ConpherenceTransactionRenderer extends Phobject {
 
   public static function renderTransactions(
     PhabricatorUser $user,
     ConpherenceThread $conpherence,
     $full_display = true,
     $marker_type = 'older') {
 
     $transactions = $conpherence->getTransactions();
 
     $oldest_transaction_id = 0;
     $newest_transaction_id = 0;
     $too_many = ConpherenceThreadQuery::TRANSACTION_LIMIT + 1;
     if (count($transactions) == $too_many) {
       if ($marker_type == 'olderandnewer') {
         $last_transaction = end($transactions);
         $first_transaction = reset($transactions);
         unset($transactions[$last_transaction->getID()]);
         unset($transactions[$first_transaction->getID()]);
         $oldest_transaction_id = $last_transaction->getID();
         $newest_transaction_id = $first_transaction->getID();
       } else if ($marker_type == 'newer') {
         $first_transaction = reset($transactions);
         unset($transactions[$first_transaction->getID()]);
         $newest_transaction_id = $first_transaction->getID();
       } else if ($marker_type == 'older') {
         $last_transaction = end($transactions);
         unset($transactions[$last_transaction->getID()]);
         $oldest_transaction = end($transactions);
         $oldest_transaction_id = $oldest_transaction->getID();
       }
     // we need **at least** the newer marker in this mode even if
     // we didn't get a full set of transactions
     } else if ($marker_type == 'olderandnewer') {
       $first_transaction = reset($transactions);
       unset($transactions[$first_transaction->getID()]);
       $newest_transaction_id = $first_transaction->getID();
     }
 
     $transactions = array_reverse($transactions);
     $handles = $conpherence->getHandles();
     $rendered_transactions = array();
     $engine = id(new PhabricatorMarkupEngine())
       ->setViewer($user)
       ->setContextObject($conpherence);
     foreach ($transactions as $key => $transaction) {
       if ($transaction->shouldHide()) {
         unset($transactions[$key]);
         continue;
       }
       if ($transaction->getComment()) {
         $engine->addObject(
           $transaction->getComment(),
           PhabricatorApplicationTransactionComment::MARKUP_FIELD_COMMENT);
       }
     }
     $engine->process();
     // we're going to insert a dummy date marker transaction for breaks
     // between days. some setup required!
     $previous_transaction = null;
     $date_marker_transaction = id(new ConpherenceTransaction())
       ->setTransactionType(ConpherenceTransaction::TYPE_DATE_MARKER)
       ->makeEphemeral();
     $date_marker_transaction_view = id(new ConpherenceTransactionView())
       ->setUser($user)
       ->setConpherenceTransaction($date_marker_transaction)
       ->setConpherenceThread($conpherence)
       ->setHandles($handles)
       ->setMarkupEngine($engine);
 
     $transaction_view_template = id(new ConpherenceTransactionView())
       ->setUser($user)
       ->setConpherenceThread($conpherence)
       ->setHandles($handles)
       ->setMarkupEngine($engine)
       ->setFullDisplay($full_display);
 
     foreach ($transactions as $transaction) {
       if ($previous_transaction) {
         $previous_day = phabricator_format_local_time(
           $previous_transaction->getDateCreated(),
           $user,
           'Ymd');
         $current_day = phabricator_format_local_time(
           $transaction->getDateCreated(),
           $user,
           'Ymd');
         // date marker transaction time!
         if ($previous_day != $current_day) {
           $date_marker_transaction->setDateCreated(
             $transaction->getDateCreated());
           $date_marker_transaction->setID($previous_transaction->getID());
           $rendered_transactions[] = $date_marker_transaction_view->render();
         }
       }
       $transaction_view = id(clone $transaction_view_template)
         ->setConpherenceTransaction($transaction);
 
       $rendered_transactions[] = $transaction_view->render();
       $previous_transaction = $transaction;
     }
     $latest_transaction_id = $transaction->getID();
 
     return array(
       'transactions' => $rendered_transactions,
       'latest_transaction' => $transaction,
       'latest_transaction_id' => $latest_transaction_id,
       'oldest_transaction_id' => $oldest_transaction_id,
       'newest_transaction_id' => $newest_transaction_id,
     );
   }
 
   public static function renderMessagePaneContent(
     array $transactions,
     $oldest_transaction_id,
     $newest_transaction_id) {
 
     $oldscrollbutton = '';
     if ($oldest_transaction_id) {
       $oldscrollbutton = javelin_tag(
         'a',
         array(
           'href' => '#',
           'mustcapture' => true,
           'sigil' => 'show-older-messages',
           'class' => 'conpherence-show-more-messages',
           'meta' => array(
             'oldest_transaction_id' => $oldest_transaction_id,
           ),
         ),
         pht('Show Older Messages'));
       $oldscrollbutton = javelin_tag(
         'div',
         array(
           'sigil' => 'conpherence-transaction-view',
           'meta' => array(
             'id' => $oldest_transaction_id - 0.5,
           ),
         ),
         $oldscrollbutton);
     }
 
     $newscrollbutton = '';
     if ($newest_transaction_id) {
       $newscrollbutton = javelin_tag(
         'a',
         array(
           'href' => '#',
           'mustcapture' => true,
           'sigil' => 'show-newer-messages',
           'class' => 'conpherence-show-more-messages',
           'meta' => array(
             'newest_transaction_id' => $newest_transaction_id,
           ),
         ),
         pht('Show Newer Messages'));
       $newscrollbutton = javelin_tag(
         'div',
         array(
           'sigil' => 'conpherence-transaction-view',
           'meta' => array(
             'id' => $newest_transaction_id + 0.5,
           ),
         ),
         $newscrollbutton);
     }
 
     return hsprintf(
       '%s%s%s',
       $oldscrollbutton,
       $transactions,
       $newscrollbutton);
   }
 
 }
diff --git a/src/applications/conpherence/constants/ConpherenceConstants.php b/src/applications/conpherence/constants/ConpherenceConstants.php
index 01c884b2c..1dba39002 100644
--- a/src/applications/conpherence/constants/ConpherenceConstants.php
+++ b/src/applications/conpherence/constants/ConpherenceConstants.php
@@ -1,3 +1,3 @@
 <?php
 
-abstract class ConpherenceConstants {}
+abstract class ConpherenceConstants extends Phobject {}
diff --git a/src/applications/conpherence/policyrule/ConpherenceThreadMembersPolicyRule.php b/src/applications/conpherence/policyrule/ConpherenceThreadMembersPolicyRule.php
new file mode 100644
index 000000000..658ee42bd
--- /dev/null
+++ b/src/applications/conpherence/policyrule/ConpherenceThreadMembersPolicyRule.php
@@ -0,0 +1,42 @@
+<?php
+
+final class ConpherenceThreadMembersPolicyRule
+  extends PhabricatorPolicyRule {
+
+  public function getObjectPolicyKey() {
+    return 'conpherence.members';
+  }
+
+  public function getObjectPolicyName() {
+    return pht('Thread Members');
+  }
+
+  public function getPolicyExplanation() {
+    return pht('Members of this thread can take this action.');
+  }
+
+  public function getRuleDescription() {
+    return pht('thread members');
+  }
+
+  public function canApplyToObject(PhabricatorPolicyInterface $object) {
+    return ($object instanceof ConpherenceThread);
+  }
+
+  public function applyRule(
+    PhabricatorUser $viewer,
+    $value,
+    PhabricatorPolicyInterface $object) {
+    $viewer_phid = $viewer->getPHID();
+    if (!$viewer_phid) {
+      return false;
+    }
+
+    return (bool)$object->getParticipantIfExists($viewer_phid);
+  }
+
+  public function getValueControlType() {
+    return self::CONTROL_TYPE_NONE;
+  }
+
+}
diff --git a/src/applications/console/core/DarkConsoleCore.php b/src/applications/console/core/DarkConsoleCore.php
index 321b681d5..6696eb453 100644
--- a/src/applications/console/core/DarkConsoleCore.php
+++ b/src/applications/console/core/DarkConsoleCore.php
@@ -1,134 +1,134 @@
 <?php
 
-final class DarkConsoleCore {
+final class DarkConsoleCore extends Phobject {
 
   private $plugins = array();
   const STORAGE_VERSION = 1;
 
   public function __construct() {
     $symbols = id(new PhutilSymbolLoader())
       ->setType('class')
       ->setAncestorClass('DarkConsolePlugin')
       ->selectAndLoadSymbols();
 
     foreach ($symbols as $symbol) {
       $plugin = newv($symbol['name'], array());
       if (!$plugin->shouldStartup()) {
         continue;
       }
       $plugin->setConsoleCore($this);
       $plugin->didStartup();
       $this->plugins[$symbol['name']] = $plugin;
     }
   }
 
   public function getPlugins() {
     return $this->plugins;
   }
 
   public function getKey(AphrontRequest $request) {
     $plugins = $this->getPlugins();
 
     foreach ($plugins as $plugin) {
       $plugin->setRequest($request);
       $plugin->willShutdown();
     }
 
     foreach ($plugins as $plugin) {
       $plugin->didShutdown();
     }
 
     foreach ($plugins as $plugin) {
       $plugin->setData($plugin->generateData());
     }
 
     $plugins = msort($plugins, 'getOrderKey');
 
     $key = Filesystem::readRandomCharacters(24);
 
     $tabs = array();
     $data = array();
     foreach ($plugins as $plugin) {
       $class = get_class($plugin);
       $tabs[] = array(
         'class' => $class,
         'name'  => $plugin->getName(),
         'color' => $plugin->getColor(),
       );
       $data[$class] = $this->sanitizeForJSON($plugin->getData());
     }
 
     $storage = array(
       'vers' => self::STORAGE_VERSION,
       'tabs' => $tabs,
       'data' => $data,
       'user' => $request->getUser()
         ? $request->getUser()->getPHID()
         : null,
     );
 
     $cache = new PhabricatorKeyValueDatabaseCache();
     $cache = new PhutilKeyValueCacheProfiler($cache);
     $cache->setProfiler(PhutilServiceProfiler::getInstance());
 
     // This encoding may fail if there are, e.g., database queries which
     // include binary data. It would be a little cleaner to try to strip these,
     // but just do something non-broken here if we end up with unrepresentable
     // data.
     $json = @json_encode($storage);
     if (!$json) {
       $json = '{}';
     }
 
     $cache->setKeys(
       array(
         'darkconsole:'.$key => $json,
       ),
       $ttl = (60 * 60 * 6));
 
     return $key;
   }
 
   public function getColor() {
     foreach ($this->getPlugins() as $plugin) {
       if ($plugin->getColor()) {
         return $plugin->getColor();
       }
     }
   }
 
   public function render(AphrontRequest $request) {
     $user = $request->getUser();
     $visible = $user ? $user->getConsoleVisible() : true;
 
     return javelin_tag(
       'div',
       array(
         'id' => 'darkconsole',
         'class' => 'dark-console',
         'style' => $visible ? '' : 'display: none;',
         'data-console-key' => $this->getKey($request),
         'data-console-color' => $this->getColor(),
       ),
       '');
   }
 
   /**
    * Sometimes, tab data includes binary information (like INSERT queries which
    * write file data into the database). To successfully JSON encode it, we
    * need to convert it to UTF-8.
    */
   private function sanitizeForJSON($data) {
     if (is_object($data)) {
       return '<object:'.get_class($data).'>';
     } else if (is_array($data)) {
       foreach ($data as $key => $value) {
         $data[$key] = $this->sanitizeForJSON($value);
       }
       return $data;
     } else {
       return phutil_utf8ize($data);
     }
   }
 
 }
diff --git a/src/applications/console/plugin/DarkConsolePlugin.php b/src/applications/console/plugin/DarkConsolePlugin.php
index be1eeb846..13d237d9d 100644
--- a/src/applications/console/plugin/DarkConsolePlugin.php
+++ b/src/applications/console/plugin/DarkConsolePlugin.php
@@ -1,85 +1,85 @@
 <?php
 
-abstract class DarkConsolePlugin {
+abstract class DarkConsolePlugin extends Phobject {
 
   private $data;
   private $request;
   private $core;
 
   abstract public function getName();
   abstract public function getDescription();
   abstract public function renderPanel();
 
   public function __construct() {}
 
   public function getColor() {
     return null;
   }
 
   final public function getOrderKey() {
     return sprintf(
       '%09d%s',
       (int)(999999999 * $this->getOrder()),
       $this->getName());
   }
 
   public function getOrder() {
     return 1.0;
   }
 
   public function setConsoleCore(DarkConsoleCore $core) {
     $this->core = $core;
     return $this;
   }
 
   public function getConsoleCore() {
     return $this->core;
   }
 
   public function generateData() {
     return null;
   }
 
   public function setData($data) {
     $this->data = $data;
     return $this;
   }
 
   public function getData() {
     return $this->data;
   }
 
   public function setRequest($request) {
     $this->request = $request;
     return $this;
   }
 
   public function getRequest() {
     return $this->request;
   }
 
   public function getRequestURI() {
     return $this->getRequest()->getRequestURI();
   }
 
   public function shouldStartup() {
     return true;
   }
 
   public function didStartup() {
     return null;
   }
 
   public function willShutdown() {
     return null;
   }
 
   public function didShutdown() {
     return null;
   }
 
   public function processRequest() {
     return null;
   }
 
 }
diff --git a/src/applications/console/plugin/errorlog/DarkConsoleErrorLogPluginAPI.php b/src/applications/console/plugin/errorlog/DarkConsoleErrorLogPluginAPI.php
index 3067472cf..bc276da0d 100644
--- a/src/applications/console/plugin/errorlog/DarkConsoleErrorLogPluginAPI.php
+++ b/src/applications/console/plugin/errorlog/DarkConsoleErrorLogPluginAPI.php
@@ -1,75 +1,75 @@
 <?php
 
-final class DarkConsoleErrorLogPluginAPI {
+final class DarkConsoleErrorLogPluginAPI extends Phobject {
 
   private static $errors = array();
 
   private static $discardMode = false;
 
   public static function registerErrorHandler() {
     // NOTE: This forces PhutilReadableSerializer to load, so that we are
     // able to handle errors which fire from inside autoloaders (PHP will not
     // reenter autoloaders).
     PhutilReadableSerializer::printableValue(null);
     PhutilErrorHandler::setErrorListener(
       array(__CLASS__, 'handleErrors'));
   }
 
   public static function enableDiscardMode() {
     self::$discardMode = true;
   }
 
   public static function disableDiscardMode() {
     self::$discardMode = false;
   }
 
   public static function getErrors() {
     return self::$errors;
   }
 
   public static function handleErrors($event, $value, $metadata) {
     if (self::$discardMode) {
       return;
     }
 
     switch ($event) {
       case PhutilErrorHandler::EXCEPTION:
         // $value is of type Exception
         self::$errors[] = array(
           'details'   => $value->getMessage(),
           'event'     => $event,
           'file'      => $value->getFile(),
           'line'      => $value->getLine(),
           'str'       => $value->getMessage(),
           'trace'     => $metadata['trace'],
         );
         break;
       case PhutilErrorHandler::ERROR:
         // $value is a simple string
         self::$errors[] = array(
           'details'   => $value,
           'event'     => $event,
           'file'      => $metadata['file'],
           'line'      => $metadata['line'],
           'str'       => $value,
           'trace'     => $metadata['trace'],
         );
         break;
       case PhutilErrorHandler::PHLOG:
         // $value can be anything
         self::$errors[] = array(
           'details' => PhutilReadableSerializer::printShallow($value, 3),
           'event'   => $event,
           'file'    => $metadata['file'],
           'line'    => $metadata['line'],
           'str'     => PhutilReadableSerializer::printShort($value),
           'trace'   => $metadata['trace'],
         );
         break;
       default:
         error_log(pht('Unknown event: %s', $event));
         break;
     }
   }
 
 }
diff --git a/src/applications/console/plugin/xhprof/DarkConsoleXHProfPluginAPI.php b/src/applications/console/plugin/xhprof/DarkConsoleXHProfPluginAPI.php
index a89c5a37b..ea2f1bbad 100644
--- a/src/applications/console/plugin/xhprof/DarkConsoleXHProfPluginAPI.php
+++ b/src/applications/console/plugin/xhprof/DarkConsoleXHProfPluginAPI.php
@@ -1,186 +1,186 @@
 <?php
 
 /**
  * @phutil-external-symbol function xhprof_enable
  * @phutil-external-symbol function xhprof_disable
  */
-final class DarkConsoleXHProfPluginAPI {
+final class DarkConsoleXHProfPluginAPI extends Phobject {
 
   private static $profilerStarted;
   private static $profilerRunning;
   private static $profileFilePHID;
 
   public static function isProfilerAvailable() {
     return extension_loaded('xhprof');
   }
 
   public static function getProfilerHeader() {
     return 'X-Phabricator-Profiler';
   }
 
   public static function isProfilerRequested() {
     if (!empty($_REQUEST['__profile__'])) {
       return $_REQUEST['__profile__'];
     }
 
     $header = AphrontRequest::getHTTPHeader(self::getProfilerHeader());
     if ($header) {
       return $header;
     }
 
     return false;
   }
 
   private static function shouldStartProfiler() {
     if (self::isProfilerRequested()) {
       return true;
     }
 
     static $sample_request = null;
 
     if ($sample_request === null) {
       if (PhabricatorEnv::getEnvConfig('debug.profile-rate')) {
         $rate = PhabricatorEnv::getEnvConfig('debug.profile-rate');
         if (mt_rand(1, $rate) == 1) {
           $sample_request = true;
         } else {
           $sample_request = false;
         }
       }
     }
 
     return $sample_request;
   }
 
   public static function isProfilerStarted() {
     return self::$profilerStarted;
   }
 
   private static function isProfilerRunning() {
     return self::$profilerRunning;
   }
 
   public static function includeXHProfLib() {
     // TODO: this is incredibly stupid, but we may not have Phutil metamodule
     // stuff loaded yet so we can't just phutil_get_library_root() our way
     // to victory.
     $root = __FILE__;
     for ($ii = 0; $ii < 6; $ii++) {
       $root = dirname($root);
     }
 
     require_once $root.'/externals/xhprof/xhprof_lib.php';
   }
 
 
   public static function saveProfilerSample(PhutilDeferredLog $access_log) {
     $file_phid = self::getProfileFilePHID();
     if (!$file_phid) {
       return;
     }
 
     if (self::isProfilerRequested()) {
       $sample_rate = 0;
     } else {
       $sample_rate = PhabricatorEnv::getEnvConfig('debug.profile-rate');
     }
 
     $profile_sample = id(new PhabricatorXHProfSample())
       ->setFilePHID($file_phid)
       ->setSampleRate($sample_rate)
       ->setUsTotal($access_log->getData('T'))
       ->setHostname($access_log->getData('h'))
       ->setRequestPath($access_log->getData('U'))
       ->setController($access_log->getData('C'))
       ->setUserPHID($access_log->getData('P'));
 
     AphrontWriteGuard::allowDangerousUnguardedWrites(true);
       $caught = null;
       try {
         $profile_sample->save();
       } catch (Exception $ex) {
         $caught = $ex;
       }
     AphrontWriteGuard::allowDangerousUnguardedWrites(false);
 
     if ($caught) {
       throw $caught;
     }
   }
 
   public static function hookProfiler() {
     if (!self::shouldStartProfiler()) {
       return;
     }
 
     if (!self::isProfilerAvailable()) {
       return;
     }
 
     if (self::$profilerStarted) {
       return;
     }
 
     self::startProfiler();
   }
 
   private static function startProfiler() {
     self::includeXHProfLib();
     xhprof_enable();
 
     self::$profilerStarted = true;
     self::$profilerRunning = true;
   }
 
   public static function getProfileFilePHID() {
     self::stopProfiler();
     return self::$profileFilePHID;
   }
 
   private static function stopProfiler() {
     if (!self::isProfilerRunning()) {
       return;
     }
 
     $data = xhprof_disable();
     $data = @json_encode($data);
     self::$profilerRunning = false;
 
     // Since these happen on GET we can't do guarded writes. These also
     // sometimes happen after we've disposed of the write guard; in this
     // case we need to disable the whole mechanism.
 
     $use_scope = AphrontWriteGuard::isGuardActive();
     if ($use_scope) {
       $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
     } else {
       AphrontWriteGuard::allowDangerousUnguardedWrites(true);
     }
 
     $caught = null;
     try {
       $file = call_user_func(
         array('PhabricatorFile', 'newFromFileData'),
         $data,
         array(
           'mime-type' => 'application/xhprof',
           'name'      => 'profile.xhprof',
         ));
     } catch (Exception $ex) {
       $caught = $ex;
     }
 
     if ($use_scope) {
       unset($unguarded);
     } else {
       AphrontWriteGuard::allowDangerousUnguardedWrites(false);
     }
 
     if ($caught) {
       throw $caught;
     }
 
     self::$profileFilePHID = $file->getPHID();
   }
 
 }
diff --git a/src/applications/countdown/application/PhabricatorCountdownApplication.php b/src/applications/countdown/application/PhabricatorCountdownApplication.php
index 5c00661bd..24e796a4a 100644
--- a/src/applications/countdown/application/PhabricatorCountdownApplication.php
+++ b/src/applications/countdown/application/PhabricatorCountdownApplication.php
@@ -1,59 +1,60 @@
 <?php
 
 final class PhabricatorCountdownApplication extends PhabricatorApplication {
 
   public function getBaseURI() {
     return '/countdown/';
   }
 
   public function getFontIcon() {
     return 'fa-rocket';
   }
 
   public function getName() {
     return pht('Countdown');
   }
 
   public function getShortDescription() {
     return pht('Countdown to Events');
   }
 
   public function getTitleGlyph() {
     return "\xE2\x9A\xB2";
   }
 
   public function getFlavorText() {
     return pht('Utilize the full capabilities of your ALU.');
   }
 
   public function getApplicationGroup() {
     return self::GROUP_UTILITIES;
   }
 
   public function getRemarkupRules() {
     return array(
       new PhabricatorCountdownRemarkupRule(),
     );
   }
 
   public function getRoutes() {
     return array(
       '/countdown/' => array(
         '(?:query/(?P<queryKey>[^/]+)/)?'
           => 'PhabricatorCountdownListController',
         '(?P<id>[1-9]\d*)/' => 'PhabricatorCountdownViewController',
         'edit/(?:(?P<id>[1-9]\d*)/)?' => 'PhabricatorCountdownEditController',
         'delete/(?P<id>[1-9]\d*)/' => 'PhabricatorCountdownDeleteController',
       ),
     );
   }
 
   protected function getCustomCapabilities() {
     return array(
       PhabricatorCountdownDefaultViewCapability::CAPABILITY => array(
         'caption' => pht('Default view policy for new countdowns.'),
+        'template' => PhabricatorCountdownCountdownPHIDType::TYPECONST,
       ),
     );
   }
 
 }
diff --git a/src/applications/dashboard/layoutconfig/PhabricatorDashboardLayoutConfig.php b/src/applications/dashboard/layoutconfig/PhabricatorDashboardLayoutConfig.php
index 9909515f0..0913be81d 100644
--- a/src/applications/dashboard/layoutconfig/PhabricatorDashboardLayoutConfig.php
+++ b/src/applications/dashboard/layoutconfig/PhabricatorDashboardLayoutConfig.php
@@ -1,164 +1,164 @@
 <?php
 
-final class PhabricatorDashboardLayoutConfig {
+final class PhabricatorDashboardLayoutConfig extends Phobject {
 
   const MODE_FULL                = 'layout-mode-full';
   const MODE_HALF_AND_HALF       = 'layout-mode-half-and-half';
   const MODE_THIRD_AND_THIRDS    = 'layout-mode-third-and-thirds';
   const MODE_THIRDS_AND_THIRD    = 'layout-mode-thirds-and-third';
 
   private $layoutMode     = self::MODE_FULL;
   private $panelLocations = array();
 
   public function setLayoutMode($mode) {
     $this->layoutMode = $mode;
     return $this;
   }
   public function getLayoutMode() {
     return $this->layoutMode;
   }
 
   public function setPanelLocation($which_column, $panel_phid) {
     $this->panelLocations[$which_column][] = $panel_phid;
     return $this;
   }
 
   public function setPanelLocations(array $locations) {
     $this->panelLocations = $locations;
     return $this;
   }
 
   public function getPanelLocations() {
     return $this->panelLocations;
   }
 
   public function replacePanel($old_phid, $new_phid) {
     $locations = $this->getPanelLocations();
     foreach ($locations as $column => $panel_phids) {
       foreach ($panel_phids as $key => $panel_phid) {
         if ($panel_phid == $old_phid) {
           $locations[$column][$key] = $new_phid;
         }
       }
     }
     return $this->setPanelLocations($locations);
   }
 
   public function removePanel($panel_phid) {
     $panel_location_grid = $this->getPanelLocations();
     foreach ($panel_location_grid as $column => $panel_columns) {
       $found_old_column = array_search($panel_phid, $panel_columns);
       if ($found_old_column !== false) {
         $new_panel_columns = $panel_columns;
         array_splice(
           $new_panel_columns,
           $found_old_column,
           1,
           array());
         $panel_location_grid[$column] = $new_panel_columns;
         break;
       }
     }
     $this->setPanelLocations($panel_location_grid);
   }
 
   public function getDefaultPanelLocations() {
     switch ($this->getLayoutMode()) {
       case self::MODE_HALF_AND_HALF:
       case self::MODE_THIRD_AND_THIRDS:
       case self::MODE_THIRDS_AND_THIRD:
         $locations = array(array(), array());
         break;
       case self::MODE_FULL:
       default:
         $locations = array(array());
         break;
     }
     return $locations;
   }
 
   public function getColumnClass($column_index, $grippable = false) {
     switch ($this->getLayoutMode()) {
       case self::MODE_HALF_AND_HALF:
         $class = 'half';
         break;
       case self::MODE_THIRD_AND_THIRDS:
         if ($column_index) {
           $class = 'thirds';
         } else {
           $class = 'third';
         }
         break;
       case self::MODE_THIRDS_AND_THIRD:
         if ($column_index) {
           $class = 'third';
         } else {
           $class = 'thirds';
         }
         break;
       case self::MODE_FULL:
       default:
         $class = null;
         break;
     }
     if ($grippable) {
       $class .= ' grippable';
     }
     return $class;
   }
 
   public function isMultiColumnLayout() {
     return $this->getLayoutMode() != self::MODE_FULL;
   }
 
   public function getColumnSelectOptions() {
     $options = array();
 
     switch ($this->getLayoutMode()) {
       case self::MODE_HALF_AND_HALF:
       case self::MODE_THIRD_AND_THIRDS:
       case self::MODE_THIRDS_AND_THIRD:
         return array(
           0 => pht('Left'),
           1 => pht('Right'),
         );
         break;
       case self::MODE_FULL:
         throw new Exception(pht('There is only one column in mode full.'));
         break;
       default:
         throw new Exception(pht('Unknown layout mode!'));
         break;
     }
 
     return $options;
   }
 
   public static function getLayoutModeSelectOptions() {
     return array(
       self::MODE_FULL             => pht('One full-width column'),
       self::MODE_HALF_AND_HALF    => pht('Two columns, 1/2 and 1/2'),
       self::MODE_THIRD_AND_THIRDS => pht('Two columns, 1/3 and 2/3'),
       self::MODE_THIRDS_AND_THIRD => pht('Two columns, 2/3 and 1/3'),
     );
   }
 
   public static function newFromDictionary(array $dict) {
     $layout_config = id(new PhabricatorDashboardLayoutConfig())
       ->setLayoutMode(idx($dict, 'layoutMode', self::MODE_FULL));
     $layout_config->setPanelLocations(idx(
       $dict,
       'panelLocations',
       $layout_config->getDefaultPanelLocations()));
 
     return $layout_config;
   }
 
   public function toDictionary() {
     return array(
       'layoutMode' => $this->getLayoutMode(),
       'panelLocations' => $this->getPanelLocations(),
     );
   }
 
 }
diff --git a/src/applications/differential/DifferentialGetWorkingCopy.php b/src/applications/differential/DifferentialGetWorkingCopy.php
index 7708d8483..3d2c911cc 100644
--- a/src/applications/differential/DifferentialGetWorkingCopy.php
+++ b/src/applications/differential/DifferentialGetWorkingCopy.php
@@ -1,73 +1,73 @@
 <?php
 
 /**
  * Can't find a good place for this, so I'm putting it in the most notably
  * wrong place.
  */
-final class DifferentialGetWorkingCopy {
+final class DifferentialGetWorkingCopy extends Phobject {
 
   /**
    * Creates and/or cleans a workspace for the requested repo.
    *
    * return ArcanistGitAPI
    */
   public static function getCleanGitWorkspace(
     PhabricatorRepository $repo) {
 
     $origin_path = $repo->getLocalPath();
 
     $path = rtrim($origin_path, '/');
     $path = $path.'__workspace';
 
     if (!Filesystem::pathExists($path)) {
       $repo->execxLocalCommand(
         'clone -- file://%s %s',
         $origin_path,
         $path);
 
       if (!$repo->isHosted()) {
         id(new ArcanistGitAPI($path))->execxLocal(
           'remote set-url origin %s',
           $repo->getRemoteURI());
       }
     }
 
     $workspace = new ArcanistGitAPI($path);
     $workspace->execxLocal('clean -f -d');
     $workspace->execxLocal('checkout master');
     $workspace->execxLocal('fetch');
     $workspace->execxLocal('reset --hard origin/master');
     $workspace->reloadWorkingCopy();
 
     return $workspace;
   }
 
   /**
    * Creates and/or cleans a workspace for the requested repo.
    *
    * return ArcanistMercurialAPI
    */
   public static function getCleanMercurialWorkspace(
     PhabricatorRepository $repo) {
 
     $origin_path = $repo->getLocalPath();
 
     $path = rtrim($origin_path, '/');
     $path = $path.'__workspace';
 
     if (!Filesystem::pathExists($path)) {
       $repo->execxLocalCommand(
         'clone -- file://%s %s',
         $origin_path,
         $path);
     }
 
     $workspace = new ArcanistMercurialAPI($path);
     $workspace->execxLocal('pull');
     $workspace->execxLocal('update --clean default');
     $workspace->reloadWorkingCopy();
 
     return $workspace;
   }
 
 }
diff --git a/src/applications/differential/application/PhabricatorDifferentialApplication.php b/src/applications/differential/application/PhabricatorDifferentialApplication.php
index 7bbe13342..d73236668 100644
--- a/src/applications/differential/application/PhabricatorDifferentialApplication.php
+++ b/src/applications/differential/application/PhabricatorDifferentialApplication.php
@@ -1,212 +1,213 @@
 <?php
 
 final class PhabricatorDifferentialApplication extends PhabricatorApplication {
 
   public function getBaseURI() {
     return '/differential/';
   }
 
   public function getName() {
     return pht('Differential');
   }
 
   public function getShortDescription() {
     return pht('Review Code');
   }
 
   public function getFontIcon() {
     return 'fa-cog';
   }
 
   public function isPinnedByDefault(PhabricatorUser $viewer) {
     return true;
   }
 
   public function getHelpDocumentationArticles(PhabricatorUser $viewer) {
     return array(
       array(
         'name' => pht('Differential User Guide'),
         'href' => PhabricatorEnv::getDoclink('Differential User Guide'),
       ),
     );
   }
 
   public function getFactObjectsForAnalysis() {
     return array(
       new DifferentialRevision(),
     );
   }
 
   public function getTitleGlyph() {
     return "\xE2\x9A\x99";
   }
 
   public function getEventListeners() {
     return array(
       new DifferentialActionMenuEventListener(),
       new DifferentialHovercardEventListener(),
       new DifferentialLandingActionMenuEventListener(),
     );
   }
 
   public function getOverview() {
     return pht(
       'Differential is a **code review application** which allows '.
       'engineers to review, discuss and approve changes to software.');
   }
 
   public function getRoutes() {
     return array(
       '/D(?P<id>[1-9]\d*)' => 'DifferentialRevisionViewController',
       '/differential/' => array(
         '(?:query/(?P<queryKey>[^/]+)/)?'
           => 'DifferentialRevisionListController',
         'diff/' => array(
           '(?P<id>[1-9]\d*)/' => 'DifferentialDiffViewController',
           'create/' => 'DifferentialDiffCreateController',
         ),
         'changeset/' => 'DifferentialChangesetViewController',
         'revision/' => array(
           'edit/(?:(?P<id>[1-9]\d*)/)?'
             => 'DifferentialRevisionEditController',
           'land/(?:(?P<id>[1-9]\d*))/(?P<strategy>[^/]+)/'
             => 'DifferentialRevisionLandController',
           'closedetails/(?P<phid>[^/]+)/'
             => 'DifferentialRevisionCloseDetailsController',
           'update/(?P<revisionID>[1-9]\d*)/'
             => 'DifferentialDiffCreateController',
         ),
         'comment/' => array(
           'preview/(?P<id>[1-9]\d*)/' => 'DifferentialCommentPreviewController',
           'save/(?P<id>[1-9]\d*)/' => 'DifferentialCommentSaveController',
           'inline/' => array(
             'preview/(?P<id>[1-9]\d*)/'
               => 'DifferentialInlineCommentPreviewController',
             'edit/(?P<id>[1-9]\d*)/'
               => 'DifferentialInlineCommentEditController',
           ),
         ),
         'preview/' => 'PhabricatorMarkupPreviewController',
       ),
     );
   }
 
   public function getApplicationOrder() {
     return 0.100;
   }
 
   public function getRemarkupRules() {
     return array(
       new DifferentialRemarkupRule(),
     );
   }
 
   public function loadStatus(PhabricatorUser $user) {
     $revisions = id(new DifferentialRevisionQuery())
       ->setViewer($user)
       ->withResponsibleUsers(array($user->getPHID()))
       ->withStatus(DifferentialRevisionQuery::STATUS_OPEN)
       ->needRelationships(true)
       ->setLimit(self::MAX_STATUS_ITEMS)
       ->execute();
 
     $status = array();
     if (count($revisions) == self::MAX_STATUS_ITEMS) {
       $all_count = count($revisions);
       $all_count_str = self::formatStatusCount(
         $all_count,
         '%s Active Reviews',
         '%d Active Review(s)');
       $type = PhabricatorApplicationStatusView::TYPE_WARNING;
       $status[] = id(new PhabricatorApplicationStatusView())
         ->setType($type)
         ->setText($all_count_str)
         ->setCount($all_count);
     } else {
       list($blocking, $active, $waiting) =
         DifferentialRevisionQuery::splitResponsible(
           $revisions,
           array($user->getPHID()));
 
       $blocking = count($blocking);
       $blocking_str = self::formatStatusCount(
         $blocking,
         '%s Reviews Blocking Others',
         '%d Review(s) Blocking Others');
       $type = PhabricatorApplicationStatusView::TYPE_NEEDS_ATTENTION;
       $status[] = id(new PhabricatorApplicationStatusView())
         ->setType($type)
         ->setText($blocking_str)
         ->setCount($blocking);
 
       $active = count($active);
       $active_str = self::formatStatusCount(
         $active,
         '%s Reviews Need Attention',
         '%d Review(s) Need Attention');
       $type = PhabricatorApplicationStatusView::TYPE_WARNING;
       $status[] = id(new PhabricatorApplicationStatusView())
         ->setType($type)
         ->setText($active_str)
         ->setCount($active);
 
       $waiting = count($waiting);
       $waiting_str = self::formatStatusCount(
         $waiting,
         '%s Reviews Waiting on Others',
         '%d Review(s) Waiting on Others');
       $type = PhabricatorApplicationStatusView::TYPE_INFO;
       $status[] = id(new PhabricatorApplicationStatusView())
         ->setType($type)
         ->setText($waiting_str)
         ->setCount($waiting);
     }
 
     return $status;
   }
 
   public function supportsEmailIntegration() {
     return true;
   }
 
   public function getAppEmailBlurb() {
     return pht(
       'Send email to these addresses to create revisions. The body of the '.
       'message and / or one or more attachments should be the output of a '.
       '"diff" command. %s',
       phutil_tag(
         'a',
         array(
           'href' => $this->getInboundEmailSupportLink(),
         ),
         pht('Learn More')));
   }
 
   protected function getCustomCapabilities() {
     return array(
       DifferentialDefaultViewCapability::CAPABILITY => array(
         'caption' => pht('Default view policy for newly created revisions.'),
+        'template' => DifferentialRevisionPHIDType::TYPECONST,
       ),
     );
   }
 
   public function getMailCommandObjects() {
     return array(
       'revision' => array(
         'name' => pht('Email Commands: Revisions'),
         'header' => pht('Interacting with Differential Revisions'),
         'object' => new DifferentialRevision(),
         'summary' => pht(
           'This page documents the commands you can use to interact with '.
           'revisions in Differential.'),
       ),
     );
   }
 
   public function getApplicationSearchDocumentTypes() {
     return array(
       DifferentialRevisionPHIDType::TYPECONST,
     );
   }
 
 }
diff --git a/src/applications/differential/constants/DifferentialAction.php b/src/applications/differential/constants/DifferentialAction.php
index 5b5fa9eb7..6227d61dd 100644
--- a/src/applications/differential/constants/DifferentialAction.php
+++ b/src/applications/differential/constants/DifferentialAction.php
@@ -1,155 +1,155 @@
 <?php
 
-final class DifferentialAction {
+final class DifferentialAction extends Phobject {
 
   const ACTION_CLOSE          = 'commit';
   const ACTION_COMMENT        = 'none';
   const ACTION_ACCEPT         = 'accept';
   const ACTION_REJECT         = 'reject';
   const ACTION_RETHINK        = 'rethink';
   const ACTION_ABANDON        = 'abandon';
   const ACTION_REQUEST        = 'request_review';
   const ACTION_RECLAIM        = 'reclaim';
   const ACTION_UPDATE         = 'update';
   const ACTION_RESIGN         = 'resign';
   const ACTION_SUMMARIZE      = 'summarize';
   const ACTION_TESTPLAN       = 'testplan';
   const ACTION_CREATE         = 'create';
   const ACTION_ADDREVIEWERS   = 'add_reviewers';
   const ACTION_ADDCCS         = 'add_ccs';
   const ACTION_CLAIM          = 'claim';
   const ACTION_REOPEN         = 'reopen';
 
   public static function getBasicStoryText($action, $author_name) {
     switch ($action) {
       case self::ACTION_COMMENT:
         $title = pht(
           '%s commented on this revision.',
           $author_name);
         break;
       case self::ACTION_ACCEPT:
         $title = pht(
           '%s accepted this revision.',
           $author_name);
         break;
       case self::ACTION_REJECT:
         $title = pht(
           '%s requested changes to this revision.',
           $author_name);
         break;
       case self::ACTION_RETHINK:
         $title = pht(
           '%s planned changes to this revision.',
           $author_name);
         break;
       case self::ACTION_ABANDON:
         $title = pht(
           '%s abandoned this revision.',
           $author_name);
         break;
       case self::ACTION_CLOSE:
         $title = pht(
           '%s closed this revision.',
           $author_name);
         break;
       case self::ACTION_REQUEST:
         $title = pht(
           '%s requested a review of this revision.',
           $author_name);
         break;
       case self::ACTION_RECLAIM:
         $title = pht(
           '%s reclaimed this revision.',
           $author_name);
         break;
       case self::ACTION_UPDATE:
         $title = pht(
           '%s updated this revision.',
           $author_name);
         break;
       case self::ACTION_RESIGN:
         $title = pht(
           '%s resigned from this revision.',
           $author_name);
         break;
       case self::ACTION_SUMMARIZE:
         $title = pht(
           '%s summarized this revision.',
           $author_name);
         break;
       case self::ACTION_TESTPLAN:
         $title = pht(
           '%s explained the test plan for this revision.',
           $author_name);
         break;
       case self::ACTION_CREATE:
         $title = pht(
           '%s created this revision.',
           $author_name);
         break;
       case self::ACTION_ADDREVIEWERS:
         $title = pht(
           '%s added reviewers to this revision.',
           $author_name);
         break;
       case self::ACTION_ADDCCS:
         $title = pht(
           '%s added CCs to this revision.',
           $author_name);
         break;
       case self::ACTION_CLAIM:
         $title = pht(
           '%s commandeered this revision.',
           $author_name);
         break;
       case self::ACTION_REOPEN:
         $title = pht(
           '%s reopened this revision.',
           $author_name);
         break;
       case DifferentialTransaction::TYPE_INLINE:
         $title = pht(
           '%s added an inline comment.',
           $author_name);
         break;
       default:
         $title = pht('Ghosts happened to this revision.');
         break;
     }
     return $title;
   }
 
   public static function getActionVerb($action) {
     $verbs = array(
       self::ACTION_COMMENT        => pht('Comment'),
       self::ACTION_ACCEPT         => pht("Accept Revision \xE2\x9C\x94"),
       self::ACTION_REJECT         => pht("Request Changes \xE2\x9C\x98"),
       self::ACTION_RETHINK        => pht("Plan Changes \xE2\x9C\x98"),
       self::ACTION_ABANDON        => pht('Abandon Revision'),
       self::ACTION_REQUEST        => pht('Request Review'),
       self::ACTION_RECLAIM        => pht('Reclaim Revision'),
       self::ACTION_RESIGN         => pht('Resign as Reviewer'),
       self::ACTION_ADDREVIEWERS   => pht('Add Reviewers'),
       self::ACTION_ADDCCS         => pht('Add Subscribers'),
       self::ACTION_CLOSE          => pht('Close Revision'),
       self::ACTION_CLAIM          => pht('Commandeer Revision'),
       self::ACTION_REOPEN         => pht('Reopen'),
     );
 
     if (!empty($verbs[$action])) {
       return $verbs[$action];
     } else {
       return pht('brazenly %s', $action);
     }
   }
 
   public static function allowReviewers($action) {
     if ($action == self::ACTION_ADDREVIEWERS ||
         $action == self::ACTION_REQUEST ||
         $action == self::ACTION_RESIGN) {
       return true;
     }
     return false;
   }
 
 }
diff --git a/src/applications/differential/constants/DifferentialChangeType.php b/src/applications/differential/constants/DifferentialChangeType.php
index 7e037d29b..6f8f6ad36 100644
--- a/src/applications/differential/constants/DifferentialChangeType.php
+++ b/src/applications/differential/constants/DifferentialChangeType.php
@@ -1,111 +1,111 @@
 <?php
 
-final class DifferentialChangeType {
+final class DifferentialChangeType extends Phobject {
 
   const TYPE_ADD        = 1;
   const TYPE_CHANGE     = 2;
   const TYPE_DELETE     = 3;
   const TYPE_MOVE_AWAY  = 4;
   const TYPE_COPY_AWAY  = 5;
   const TYPE_MOVE_HERE  = 6;
   const TYPE_COPY_HERE  = 7;
   const TYPE_MULTICOPY  = 8;
   const TYPE_MESSAGE    = 9;
   const TYPE_CHILD      = 10;
 
   const FILE_TEXT       = 1;
   const FILE_IMAGE      = 2;
   const FILE_BINARY     = 3;
   const FILE_DIRECTORY  = 4;
   const FILE_SYMLINK    = 5;
   const FILE_DELETED    = 6;
   const FILE_NORMAL     = 7;
   const FILE_SUBMODULE  = 8;
 
   public static function getSummaryCharacterForChangeType($type) {
     static $types = array(
       self::TYPE_ADD        => 'A',
       self::TYPE_CHANGE     => 'M',
       self::TYPE_DELETE     => 'D',
       self::TYPE_MOVE_AWAY  => 'V',
       self::TYPE_COPY_AWAY  => 'P',
       self::TYPE_MOVE_HERE  => 'V',
       self::TYPE_COPY_HERE  => 'P',
       self::TYPE_MULTICOPY  => 'P',
       self::TYPE_MESSAGE    => 'Q',
       self::TYPE_CHILD      => '@',
     );
     return idx($types, coalesce($type, '?'), '~');
   }
 
   public static function getShortNameForFileType($type) {
     static $names = array(
       self::FILE_TEXT       => null,
       self::FILE_DIRECTORY  => 'dir',
       self::FILE_IMAGE      => 'img',
       self::FILE_BINARY     => 'bin',
       self::FILE_SYMLINK    => 'sym',
       self::FILE_SUBMODULE  => 'sub',
     );
     return idx($names, coalesce($type, '?'), '???');
   }
 
   public static function isOldLocationChangeType($type) {
     static $types = array(
       self::TYPE_MOVE_AWAY  => true,
       self::TYPE_COPY_AWAY  => true,
       self::TYPE_MULTICOPY  => true,
     );
     return isset($types[$type]);
   }
 
   public static function isNewLocationChangeType($type) {
     static $types = array(
       self::TYPE_MOVE_HERE  => true,
       self::TYPE_COPY_HERE  => true,
     );
     return isset($types[$type]);
   }
 
   public static function isDeleteChangeType($type) {
     static $types = array(
       self::TYPE_DELETE     => true,
       self::TYPE_MOVE_AWAY  => true,
       self::TYPE_MULTICOPY  => true,
     );
     return isset($types[$type]);
   }
 
   public static function isCreateChangeType($type) {
     static $types = array(
       self::TYPE_ADD        => true,
       self::TYPE_COPY_HERE  => true,
       self::TYPE_MOVE_HERE  => true,
     );
     return isset($types[$type]);
   }
 
   public static function isModifyChangeType($type) {
     static $types = array(
       self::TYPE_CHANGE     => true,
     );
     return isset($types[$type]);
   }
 
   public static function getFullNameForChangeType($type) {
     $types = array(
       self::TYPE_ADD        => pht('Added'),
       self::TYPE_CHANGE     => pht('Modified'),
       self::TYPE_DELETE     => pht('Deleted'),
       self::TYPE_MOVE_AWAY  => pht('Moved Away'),
       self::TYPE_COPY_AWAY  => pht('Copied Away'),
       self::TYPE_MOVE_HERE  => pht('Moved Here'),
       self::TYPE_COPY_HERE  => pht('Copied Here'),
       self::TYPE_MULTICOPY  => pht('Deleted After Multiple Copy'),
       self::TYPE_MESSAGE    => pht('Commit Message'),
       self::TYPE_CHILD      => pht('Contents Modified'),
     );
     return idx($types, coalesce($type, '?'), pht('Unknown'));
   }
 
 }
diff --git a/src/applications/differential/constants/DifferentialLintStatus.php b/src/applications/differential/constants/DifferentialLintStatus.php
index 5e16955f9..4b491db9c 100644
--- a/src/applications/differential/constants/DifferentialLintStatus.php
+++ b/src/applications/differential/constants/DifferentialLintStatus.php
@@ -1,13 +1,13 @@
 <?php
 
-final class DifferentialLintStatus {
+final class DifferentialLintStatus extends Phobject {
 
   const LINT_NONE             = 0;
   const LINT_OKAY             = 1;
   const LINT_WARN             = 2;
   const LINT_FAIL             = 3;
   const LINT_SKIP             = 4;
   const LINT_POSTPONED        = 5;
   const LINT_AUTO_SKIP        = 6;
 
 }
diff --git a/src/applications/differential/constants/DifferentialReviewerStatus.php b/src/applications/differential/constants/DifferentialReviewerStatus.php
index 8c19855bb..f9193ef63 100644
--- a/src/applications/differential/constants/DifferentialReviewerStatus.php
+++ b/src/applications/differential/constants/DifferentialReviewerStatus.php
@@ -1,42 +1,42 @@
 <?php
 
-final class DifferentialReviewerStatus {
+final class DifferentialReviewerStatus extends Phobject {
 
   const STATUS_BLOCKING = 'blocking';
   const STATUS_ADDED = 'added';
   const STATUS_ACCEPTED = 'accepted';
   const STATUS_REJECTED = 'rejected';
   const STATUS_COMMENTED = 'commented';
   const STATUS_ACCEPTED_OLDER = 'accepted-older';
   const STATUS_REJECTED_OLDER = 'rejected-older';
 
   /**
    * Returns the relative strength of a status, used to pick a winner when a
    * transaction group makes several status changes to a particular reviewer.
    *
    * For example, if you accept a revision and leave a comment, the transactions
    * will attempt to update you to both "commented" and "accepted". We want
    * "accepted" to win, because it's the stronger of the two.
    *
    * @param   const Reviewer status constant.
    * @return  int   Relative strength (higher is stronger).
    */
   public static function getStatusStrength($constant) {
     $map = array(
       self::STATUS_ADDED      => 1,
 
       self::STATUS_COMMENTED  => 2,
 
       self::STATUS_BLOCKING   => 3,
 
       self::STATUS_ACCEPTED_OLDER   => 4,
       self::STATUS_REJECTED_OLDER   => 4,
 
       self::STATUS_ACCEPTED   => 5,
       self::STATUS_REJECTED   => 5,
     );
 
     return idx($map, $constant, 0);
   }
 
 }
diff --git a/src/applications/differential/constants/DifferentialRevisionControlSystem.php b/src/applications/differential/constants/DifferentialRevisionControlSystem.php
index 6e819ec3e..79aa7da90 100644
--- a/src/applications/differential/constants/DifferentialRevisionControlSystem.php
+++ b/src/applications/differential/constants/DifferentialRevisionControlSystem.php
@@ -1,10 +1,10 @@
 <?php
 
 // TODO: Unify with similar Repository constants
-final class DifferentialRevisionControlSystem {
+final class DifferentialRevisionControlSystem extends Phobject {
 
   const SVN         = 'svn';
   const GIT         = 'git';
   const MERCURIAL   = 'hg';
 
 }
diff --git a/src/applications/differential/constants/DifferentialRevisionStatus.php b/src/applications/differential/constants/DifferentialRevisionStatus.php
index 61340d011..90c43ccd3 100644
--- a/src/applications/differential/constants/DifferentialRevisionStatus.php
+++ b/src/applications/differential/constants/DifferentialRevisionStatus.php
@@ -1,114 +1,114 @@
 <?php
 
 /**
  * NOTE: you probably want {@class:ArcanistDifferentialRevisionStatus}.
  * This class just contains a mapping for color within the Differential
  * application.
  */
 
-final class DifferentialRevisionStatus {
+final class DifferentialRevisionStatus extends Phobject {
 
   const COLOR_STATUS_DEFAULT = 'status';
   const COLOR_STATUS_DARK = 'status-dark';
   const COLOR_STATUS_GREEN = 'status-green';
   const COLOR_STATUS_RED = 'status-red';
 
   public static function getRevisionStatusColor($status) {
     $default = self::COLOR_STATUS_DEFAULT;
 
     $map = array(
       ArcanistDifferentialRevisionStatus::NEEDS_REVIEW   =>
         self::COLOR_STATUS_DEFAULT,
       ArcanistDifferentialRevisionStatus::NEEDS_REVISION =>
         self::COLOR_STATUS_RED,
       ArcanistDifferentialRevisionStatus::CHANGES_PLANNED =>
         self::COLOR_STATUS_RED,
       ArcanistDifferentialRevisionStatus::ACCEPTED       =>
         self::COLOR_STATUS_GREEN,
       ArcanistDifferentialRevisionStatus::CLOSED         =>
         self::COLOR_STATUS_DARK,
       ArcanistDifferentialRevisionStatus::ABANDONED      =>
         self::COLOR_STATUS_DARK,
       ArcanistDifferentialRevisionStatus::IN_PREPARATION =>
         self::COLOR_STATUS_DARK,
     );
     return idx($map, $status, $default);
   }
 
   public static function getRevisionStatusIcon($status) {
     $default = 'fa-square-o bluegrey';
 
     $map = array(
       ArcanistDifferentialRevisionStatus::NEEDS_REVIEW   =>
         'fa-square-o bluegrey',
       ArcanistDifferentialRevisionStatus::NEEDS_REVISION =>
         'fa-square-o red',
       ArcanistDifferentialRevisionStatus::CHANGES_PLANNED =>
         'fa-square-o red',
       ArcanistDifferentialRevisionStatus::ACCEPTED       =>
         'fa-square-o green',
       ArcanistDifferentialRevisionStatus::CLOSED         =>
         'fa-check-square-o',
       ArcanistDifferentialRevisionStatus::ABANDONED      =>
         'fa-check-square-o',
       ArcanistDifferentialRevisionStatus::IN_PREPARATION =>
         'fa-question-circle blue',
     );
     return idx($map, $status, $default);
   }
 
   public static function renderFullDescription($status) {
     $color = self::getRevisionStatusColor($status);
     $status_name =
       ArcanistDifferentialRevisionStatus::getNameForRevisionStatus($status);
 
     $img = id(new PHUIIconView())
       ->setIconFont(self::getRevisionStatusIcon($status));
 
     $tag = phutil_tag(
       'span',
       array(
         'class' => 'phui-header-'.$color.' plr',
       ),
       array(
         $img,
         $status_name,
       ));
 
     return $tag;
   }
 
   public static function getClosedStatuses() {
     $statuses = array(
       ArcanistDifferentialRevisionStatus::CLOSED,
       ArcanistDifferentialRevisionStatus::ABANDONED,
     );
 
     if (PhabricatorEnv::getEnvConfig('differential.close-on-accept')) {
       $statuses[] = ArcanistDifferentialRevisionStatus::ACCEPTED;
     }
 
     return $statuses;
   }
 
   public static function getOpenStatuses() {
     return array_diff(self::getAllStatuses(), self::getClosedStatuses());
   }
 
   public static function getAllStatuses() {
     return array(
       ArcanistDifferentialRevisionStatus::NEEDS_REVIEW,
       ArcanistDifferentialRevisionStatus::NEEDS_REVISION,
       ArcanistDifferentialRevisionStatus::CHANGES_PLANNED,
       ArcanistDifferentialRevisionStatus::ACCEPTED,
       ArcanistDifferentialRevisionStatus::CLOSED,
       ArcanistDifferentialRevisionStatus::ABANDONED,
       ArcanistDifferentialRevisionStatus::IN_PREPARATION,
     );
   }
 
   public static function isClosedStatus($status) {
     return in_array($status, self::getClosedStatuses());
   }
 
 }
diff --git a/src/applications/differential/constants/DifferentialUnitStatus.php b/src/applications/differential/constants/DifferentialUnitStatus.php
index 9d2da07a2..65275b82b 100644
--- a/src/applications/differential/constants/DifferentialUnitStatus.php
+++ b/src/applications/differential/constants/DifferentialUnitStatus.php
@@ -1,13 +1,13 @@
 <?php
 
-final class DifferentialUnitStatus {
+final class DifferentialUnitStatus extends Phobject {
 
   const UNIT_NONE             = 0;
   const UNIT_OKAY             = 1;
   const UNIT_WARN             = 2;
   const UNIT_FAIL             = 3;
   const UNIT_SKIP             = 4;
   const UNIT_POSTPONED        = 5;
   const UNIT_AUTO_SKIP        = 6;
 
 }
diff --git a/src/applications/differential/constants/DifferentialUnitTestResult.php b/src/applications/differential/constants/DifferentialUnitTestResult.php
index 70ff89cca..6ae919cf2 100644
--- a/src/applications/differential/constants/DifferentialUnitTestResult.php
+++ b/src/applications/differential/constants/DifferentialUnitTestResult.php
@@ -1,12 +1,12 @@
 <?php
 
-final class DifferentialUnitTestResult {
+final class DifferentialUnitTestResult extends Phobject {
 
   const RESULT_PASS         = 'pass';
   const RESULT_FAIL         = 'fail';
   const RESULT_SKIP         = 'skip';
   const RESULT_BROKEN       = 'broken';
   const RESULT_UNSOUND      = 'unsound';
   const RESULT_POSTPONED    = 'postponed';
 
 }
diff --git a/src/applications/differential/landing/DifferentialLandingStrategy.php b/src/applications/differential/landing/DifferentialLandingStrategy.php
index c138c3ebc..2cec11fef 100644
--- a/src/applications/differential/landing/DifferentialLandingStrategy.php
+++ b/src/applications/differential/landing/DifferentialLandingStrategy.php
@@ -1,87 +1,87 @@
 <?php
 
-abstract class DifferentialLandingStrategy {
+abstract class DifferentialLandingStrategy extends Phobject {
 
   abstract public function processLandRequest(
     AphrontRequest $request,
     DifferentialRevision $revision,
     PhabricatorRepository $repository);
 
   /**
    * @return PhabricatorActionView or null.
    */
   abstract public function createMenuItem(
     PhabricatorUser $viewer,
     DifferentialRevision $revision,
     PhabricatorRepository $repository);
 
   /**
    * @return PhabricatorActionView which can be attached to the revision view.
    */
   protected function createActionView($revision, $name) {
     $strategy = get_class($this);
     $revision_id = $revision->getId();
     return id(new PhabricatorActionView())
       ->setRenderAsForm(true)
       ->setWorkflow(true)
       ->setName($name)
       ->setHref("/differential/revision/land/{$revision_id}/{$strategy}/");
   }
 
   /**
    * Check if this action should be disabled, and explain why.
    *
    * By default, this method checks for push permissions, and for the
    * revision being Accepted.
    *
    * @return False for "not disabled"; human-readable text explaining why, if
    *         it is disabled.
    */
   public function isActionDisabled(
     PhabricatorUser $viewer,
     DifferentialRevision $revision,
     PhabricatorRepository $repository) {
 
     $status = $revision->getStatus();
     if ($status != ArcanistDifferentialRevisionStatus::ACCEPTED) {
       return pht('Only Accepted revisions can be landed.');
     }
 
     if (!PhabricatorPolicyFilter::hasCapability(
         $viewer,
         $repository,
         DiffusionPushCapability::CAPABILITY)) {
       return pht('You do not have permissions to push to this repository.');
     }
 
     return false;
   }
 
   /**
    * Might break if repository is not Git.
    */
   protected function getGitWorkspace(PhabricatorRepository $repository) {
     try {
       return DifferentialGetWorkingCopy::getCleanGitWorkspace($repository);
     } catch (Exception $e) {
       throw new PhutilProxyException(
         pht('Failed to allocate a workspace.'),
         $e);
     }
   }
 
   /**
    * Might break if repository is not Mercurial.
    */
   protected function getMercurialWorkspace(PhabricatorRepository $repository) {
     try {
       return DifferentialGetWorkingCopy::getCleanMercurialWorkspace(
         $repository);
     } catch (Exception $e) {
       throw new PhutilProxyException(
         pht('Failed to allocate a workspace.'),
         $e);
     }
   }
 
 }
diff --git a/src/applications/differential/management/PhabricatorHunksManagementMigrateWorkflow.php b/src/applications/differential/management/PhabricatorHunksManagementMigrateWorkflow.php
index 552376d2e..90c0be171 100644
--- a/src/applications/differential/management/PhabricatorHunksManagementMigrateWorkflow.php
+++ b/src/applications/differential/management/PhabricatorHunksManagementMigrateWorkflow.php
@@ -1,62 +1,60 @@
 <?php
 
 final class PhabricatorHunksManagementMigrateWorkflow
   extends PhabricatorHunksManagementWorkflow {
 
   protected function didConstruct() {
     $this
       ->setName('migrate')
       ->setExamples('**migrate**')
       ->setSynopsis(pht('Migrate hunks to modern storage.'))
       ->setArguments(array());
   }
 
   public function execute(PhutilArgumentParser $args) {
     $saw_any_rows = false;
     $console = PhutilConsole::getConsole();
 
     $table = new DifferentialLegacyHunk();
     foreach (new LiskMigrationIterator($table) as $hunk) {
       $saw_any_rows = true;
 
       $id = $hunk->getID();
       $console->writeOut("%s\n", pht('Migrating hunk %d...', $id));
 
       $new_hunk = id(new DifferentialModernHunk())
         ->setChangesetID($hunk->getChangesetID())
         ->setOldOffset($hunk->getOldOffset())
         ->setOldLen($hunk->getOldLen())
         ->setNewOffset($hunk->getNewOffset())
         ->setNewLen($hunk->getNewLen())
         ->setChanges($hunk->getChanges())
         ->setDateCreated($hunk->getDateCreated())
         ->setDateModified($hunk->getDateModified());
 
       $hunk->openTransaction();
         $new_hunk->save();
         $hunk->delete();
       $hunk->saveTransaction();
 
       $old_len = strlen($hunk->getChanges());
       $new_len = strlen($new_hunk->getData());
       if ($old_len) {
         $diff_len = ($old_len - $new_len);
         $console->writeOut(
           "%s\n",
           pht(
             'Saved %s bytes (%s).',
             new PhutilNumber($diff_len),
             sprintf('%.1f%%', 100 * ($diff_len / $old_len))));
       }
-
-      break;
     }
 
     if ($saw_any_rows) {
       $console->writeOut("%s\n", pht('Done.'));
     } else {
       $console->writeOut("%s\n", pht('No rows to migrate.'));
     }
   }
 
 }
diff --git a/src/applications/differential/parser/DifferentialChangesetParser.php b/src/applications/differential/parser/DifferentialChangesetParser.php
index a34fe8506..6b5ea1c0d 100644
--- a/src/applications/differential/parser/DifferentialChangesetParser.php
+++ b/src/applications/differential/parser/DifferentialChangesetParser.php
@@ -1,1569 +1,1571 @@
 <?php
 
-final class DifferentialChangesetParser {
+final class DifferentialChangesetParser extends Phobject {
 
   const HIGHLIGHT_BYTE_LIMIT = 262144;
 
   protected $visible      = array();
   protected $new          = array();
   protected $old          = array();
   protected $intra        = array();
   protected $newRender    = null;
   protected $oldRender    = null;
 
   protected $filename     = null;
   protected $hunkStartLines = array();
 
   protected $comments     = array();
   protected $specialAttributes = array();
 
   protected $changeset;
   protected $whitespaceMode = null;
 
   protected $renderCacheKey = null;
 
   private $handles = array();
   private $user;
 
   private $leftSideChangesetID;
   private $leftSideAttachesToNewFile;
 
   private $rightSideChangesetID;
   private $rightSideAttachesToNewFile;
 
   private $originalLeft;
   private $originalRight;
 
   private $renderingReference;
   private $isSubparser;
 
   private $isTopLevel;
 
   private $coverage;
   private $markupEngine;
   private $highlightErrors;
   private $disableCache;
   private $renderer;
   private $characterEncoding;
   private $highlightAs;
   private $highlightingDisabled;
   private $showEditAndReplyLinks = true;
   private $canMarkDone;
   private $objectOwnerPHID;
 
   private $rangeStart;
   private $rangeEnd;
   private $mask;
 
+  private $highlightEngine;
+
   public function setRange($start, $end) {
     $this->rangeStart = $start;
     $this->rangeEnd = $end;
     return $this;
   }
 
   public function setMask(array $mask) {
     $this->mask = $mask;
     return $this;
   }
 
   public function renderChangeset() {
     return $this->render($this->rangeStart, $this->rangeEnd, $this->mask);
   }
 
   public function setShowEditAndReplyLinks($bool) {
     $this->showEditAndReplyLinks = $bool;
     return $this;
   }
 
   public function getShowEditAndReplyLinks() {
     return $this->showEditAndReplyLinks;
   }
 
   public function setHighlightAs($highlight_as) {
     $this->highlightAs = $highlight_as;
     return $this;
   }
 
   public function getHighlightAs() {
     return $this->highlightAs;
   }
 
   public function setCharacterEncoding($character_encoding) {
     $this->characterEncoding = $character_encoding;
     return $this;
   }
 
   public function getCharacterEncoding() {
     return $this->characterEncoding;
   }
 
   public function setRenderer(DifferentialChangesetRenderer $renderer) {
     $this->renderer = $renderer;
     return $this;
   }
 
   public function getRenderer() {
     if (!$this->renderer) {
       return new DifferentialChangesetTwoUpRenderer();
     }
     return $this->renderer;
   }
 
   public function setDisableCache($disable_cache) {
     $this->disableCache = $disable_cache;
     return $this;
   }
 
   public function getDisableCache() {
     return $this->disableCache;
   }
 
   public function setCanMarkDone($can_mark_done) {
     $this->canMarkDone = $can_mark_done;
     return $this;
   }
 
   public function getCanMarkDone() {
     return $this->canMarkDone;
   }
 
   public function setObjectOwnerPHID($phid) {
     $this->objectOwnerPHID = $phid;
     return $this;
   }
 
   public function getObjectOwnerPHID() {
     return $this->objectOwnerPHID;
   }
 
   public static function getDefaultRendererForViewer(PhabricatorUser $viewer) {
     $prefs = $viewer->loadPreferences();
     $pref_unified = PhabricatorUserPreferences::PREFERENCE_DIFF_UNIFIED;
     if ($prefs->getPreference($pref_unified) == 'unified') {
       return '1up';
     }
     return null;
   }
 
   public function readParametersFromRequest(AphrontRequest $request) {
     $this->setWhitespaceMode($request->getStr('whitespace'));
     $this->setCharacterEncoding($request->getStr('encoding'));
     $this->setHighlightAs($request->getStr('highlight'));
 
     $renderer = null;
 
     // If the viewer prefers unified diffs, always set the renderer to unified.
     // Otherwise, we leave it unspecified and the client will choose a
     // renderer based on the screen size.
 
     if ($request->getStr('renderer')) {
       $renderer = $request->getStr('renderer');
     } else {
       $renderer = self::getDefaultRendererForViewer($request->getViewer());
     }
 
     switch ($renderer) {
       case '1up':
         $this->setRenderer(new DifferentialChangesetOneUpRenderer());
         break;
       default:
         $this->setRenderer(new DifferentialChangesetTwoUpRenderer());
         break;
     }
 
     return $this;
   }
 
   const CACHE_VERSION = 11;
   const CACHE_MAX_SIZE = 8e6;
 
   const ATTR_GENERATED  = 'attr:generated';
   const ATTR_DELETED    = 'attr:deleted';
   const ATTR_UNCHANGED  = 'attr:unchanged';
   const ATTR_WHITELINES = 'attr:white';
   const ATTR_MOVEAWAY   = 'attr:moveaway';
 
   const LINES_CONTEXT = 8;
 
   const WHITESPACE_SHOW_ALL         = 'show-all';
   const WHITESPACE_IGNORE_TRAILING  = 'ignore-trailing';
   const WHITESPACE_IGNORE_MOST      = 'ignore-most';
   const WHITESPACE_IGNORE_ALL       = 'ignore-all';
 
   public function setOldLines(array $lines) {
     $this->old = $lines;
     return $this;
   }
 
   public function setNewLines(array $lines) {
     $this->new = $lines;
     return $this;
   }
 
   public function setSpecialAttributes(array $attributes) {
     $this->specialAttributes = $attributes;
     return $this;
   }
 
   public function setIntraLineDiffs(array $diffs) {
     $this->intra = $diffs;
     return $this;
   }
 
   public function setVisibileLinesMask(array $mask) {
     $this->visible = $mask;
     return $this;
   }
 
   /**
    * Configure which Changeset comments added to the right side of the visible
    * diff will be attached to. The ID must be the ID of a real Differential
    * Changeset.
    *
    * The complexity here is that we may show an arbitrary side of an arbitrary
    * changeset as either the left or right part of a diff. This method allows
    * the left and right halves of the displayed diff to be correctly mapped to
    * storage changesets.
    *
    * @param id    The Differential Changeset ID that comments added to the right
    *              side of the visible diff should be attached to.
    * @param bool  If true, attach new comments to the right side of the storage
    *              changeset. Note that this may be false, if the left side of
    *              some storage changeset is being shown as the right side of
    *              a display diff.
    * @return this
    */
   public function setRightSideCommentMapping($id, $is_new) {
     $this->rightSideChangesetID = $id;
     $this->rightSideAttachesToNewFile = $is_new;
     return $this;
   }
 
   /**
    * See setRightSideCommentMapping(), but this sets information for the left
    * side of the display diff.
    */
   public function setLeftSideCommentMapping($id, $is_new) {
     $this->leftSideChangesetID = $id;
     $this->leftSideAttachesToNewFile = $is_new;
     return $this;
   }
 
   public function setOriginals(
     DifferentialChangeset $left,
     DifferentialChangeset $right) {
 
     $this->originalLeft = $left;
     $this->originalRight = $right;
   }
 
   public function diffOriginals() {
     $engine = new PhabricatorDifferenceEngine();
     $changeset = $engine->generateChangesetFromFileContent(
       implode('', mpull($this->originalLeft->getHunks(), 'getChanges')),
       implode('', mpull($this->originalRight->getHunks(), 'getChanges')));
 
     $parser = new DifferentialHunkParser();
 
     return $parser->parseHunksForHighlightMasks(
       $changeset->getHunks(),
       $this->originalLeft->getHunks(),
       $this->originalRight->getHunks());
   }
 
   /**
    * Set a key for identifying this changeset in the render cache. If set, the
    * parser will attempt to use the changeset render cache, which can improve
    * performance for frequently-viewed changesets.
    *
    * By default, there is no render cache key and parsers do not use the cache.
    * This is appropriate for rarely-viewed changesets.
    *
    * NOTE: Currently, this key must be a valid Differential Changeset ID.
    *
    * @param   string  Key for identifying this changeset in the render cache.
    * @return  this
    */
   public function setRenderCacheKey($key) {
     $this->renderCacheKey = $key;
     return $this;
   }
 
   private function getRenderCacheKey() {
     return $this->renderCacheKey;
   }
 
   public function setChangeset(DifferentialChangeset $changeset) {
     $this->changeset = $changeset;
 
     $this->setFilename($changeset->getFilename());
 
     return $this;
   }
 
   public function setWhitespaceMode($whitespace_mode) {
     $this->whitespaceMode = $whitespace_mode;
     return $this;
   }
 
   public function setRenderingReference($ref) {
     $this->renderingReference = $ref;
     return $this;
   }
 
   private function getRenderingReference() {
     return $this->renderingReference;
   }
 
   public function getChangeset() {
     return $this->changeset;
   }
 
   public function setFilename($filename) {
     $this->filename = $filename;
     return $this;
   }
 
   public function setHandles(array $handles) {
     assert_instances_of($handles, 'PhabricatorObjectHandle');
     $this->handles = $handles;
     return $this;
   }
 
   public function setMarkupEngine(PhabricatorMarkupEngine $engine) {
     $this->markupEngine = $engine;
     return $this;
   }
 
   public function setUser(PhabricatorUser $user) {
     $this->user = $user;
     return $this;
   }
 
   public function getUser() {
     return $this->user;
   }
 
   public function setCoverage($coverage) {
     $this->coverage = $coverage;
     return $this;
   }
   private function getCoverage() {
     return $this->coverage;
   }
 
   public function parseInlineComment(
     PhabricatorInlineCommentInterface $comment) {
 
     // Parse only comments which are actually visible.
     if ($this->isCommentVisibleOnRenderedDiff($comment)) {
       $this->comments[] = $comment;
     }
     return $this;
   }
 
   private function loadCache() {
     $render_cache_key = $this->getRenderCacheKey();
     if (!$render_cache_key) {
       return false;
     }
 
     $data = null;
 
     $changeset = new DifferentialChangeset();
     $conn_r = $changeset->establishConnection('r');
     $data = queryfx_one(
       $conn_r,
       'SELECT * FROM %T WHERE id = %d',
       $changeset->getTableName().'_parse_cache',
       $render_cache_key);
 
     if (!$data) {
       return false;
     }
 
     if ($data['cache'][0] == '{') {
       // This is likely an old-style JSON cache which we will not be able to
       // deserialize.
       return false;
     }
 
     $data = unserialize($data['cache']);
     if (!is_array($data) || !$data) {
       return false;
     }
 
     foreach (self::getCacheableProperties() as $cache_key) {
       if (!array_key_exists($cache_key, $data)) {
         // If we're missing a cache key, assume we're looking at an old cache
         // and ignore it.
         return false;
       }
     }
 
     if ($data['cacheVersion'] !== self::CACHE_VERSION) {
       return false;
     }
 
     // Someone displays contents of a partially cached shielded file.
     if (!isset($data['newRender']) && (!$this->isTopLevel || $this->comments)) {
       return false;
     }
 
     unset($data['cacheVersion'], $data['cacheHost']);
     $cache_prop = array_select_keys($data, self::getCacheableProperties());
     foreach ($cache_prop as $cache_key => $v) {
       $this->$cache_key = $v;
     }
 
     return true;
   }
 
   protected static function getCacheableProperties() {
     return array(
       'visible',
       'new',
       'old',
       'intra',
       'newRender',
       'oldRender',
       'specialAttributes',
       'hunkStartLines',
       'cacheVersion',
       'cacheHost',
       'highlightingDisabled',
     );
   }
 
   public function saveCache() {
     if ($this->highlightErrors) {
       return false;
     }
 
     $render_cache_key = $this->getRenderCacheKey();
     if (!$render_cache_key) {
       return false;
     }
 
     $cache = array();
     foreach (self::getCacheableProperties() as $cache_key) {
       switch ($cache_key) {
         case 'cacheVersion':
           $cache[$cache_key] = self::CACHE_VERSION;
           break;
         case 'cacheHost':
           $cache[$cache_key] = php_uname('n');
           break;
         default:
           $cache[$cache_key] = $this->$cache_key;
           break;
       }
     }
     $cache = serialize($cache);
 
     // We don't want to waste too much space by a single changeset.
     if (strlen($cache) > self::CACHE_MAX_SIZE) {
       return;
     }
 
     $changeset = new DifferentialChangeset();
     $conn_w = $changeset->establishConnection('w');
 
     $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
       try {
         queryfx(
           $conn_w,
           'INSERT INTO %T (id, cache, dateCreated) VALUES (%d, %B, %d)
             ON DUPLICATE KEY UPDATE cache = VALUES(cache)',
           DifferentialChangeset::TABLE_CACHE,
           $render_cache_key,
           $cache,
           time());
       } catch (AphrontQueryException $ex) {
         // Ignore these exceptions. A common cause is that the cache is
         // larger than 'max_allowed_packet', in which case we're better off
         // not writing it.
 
         // TODO: It would be nice to tailor this more narrowly.
       }
     unset($unguarded);
   }
 
   private function markGenerated($new_corpus_block = '') {
     $generated_guess = (strpos($new_corpus_block, '@'.'generated') !== false);
 
     if (!$generated_guess) {
       $generated_path_regexps = PhabricatorEnv::getEnvConfig(
         'differential.generated-paths');
       foreach ($generated_path_regexps as $regexp) {
         if (preg_match($regexp, $this->changeset->getFilename())) {
           $generated_guess = true;
           break;
         }
       }
     }
 
     $event = new PhabricatorEvent(
       PhabricatorEventType::TYPE_DIFFERENTIAL_WILLMARKGENERATED,
       array(
         'corpus' => $new_corpus_block,
         'is_generated' => $generated_guess,
       )
     );
     PhutilEventEngine::dispatchEvent($event);
 
     $generated = $event->getValue('is_generated');
     $this->specialAttributes[self::ATTR_GENERATED] = $generated;
   }
 
   public function isGenerated() {
     return idx($this->specialAttributes, self::ATTR_GENERATED, false);
   }
 
   public function isDeleted() {
     return idx($this->specialAttributes, self::ATTR_DELETED, false);
   }
 
   public function isUnchanged() {
     return idx($this->specialAttributes, self::ATTR_UNCHANGED, false);
   }
 
   public function isWhitespaceOnly() {
     return idx($this->specialAttributes, self::ATTR_WHITELINES, false);
   }
 
   public function isMoveAway() {
     return idx($this->specialAttributes, self::ATTR_MOVEAWAY, false);
   }
 
   private function applyIntraline(&$render, $intra, $corpus) {
 
     foreach ($render as $key => $text) {
       if (isset($intra[$key])) {
         $render[$key] = ArcanistDiffUtils::applyIntralineDiff(
           $text,
           $intra[$key]);
       }
     }
   }
 
   private function getHighlightFuture($corpus) {
     $language = $this->highlightAs;
 
     if (!$language) {
       $language = $this->highlightEngine->getLanguageFromFilename(
         $this->filename);
 
       if (($language != 'txt') &&
           (strlen($corpus) > self::HIGHLIGHT_BYTE_LIMIT)) {
         $this->highlightingDisabled = true;
         $language = 'txt';
       }
     }
 
     return $this->highlightEngine->getHighlightFuture(
       $language,
       $corpus);
   }
 
   protected function processHighlightedSource($data, $result) {
 
     $result_lines = phutil_split_lines($result);
     foreach ($data as $key => $info) {
       if (!$info) {
         unset($result_lines[$key]);
       }
     }
     return $result_lines;
   }
 
   private function tryCacheStuff() {
     $whitespace_mode = $this->whitespaceMode;
     switch ($whitespace_mode) {
       case self::WHITESPACE_SHOW_ALL:
       case self::WHITESPACE_IGNORE_TRAILING:
       case self::WHITESPACE_IGNORE_ALL:
         break;
       default:
         $whitespace_mode = self::WHITESPACE_IGNORE_MOST;
         break;
     }
 
     $skip_cache = ($whitespace_mode != self::WHITESPACE_IGNORE_MOST);
     if ($this->disableCache) {
       $skip_cache = true;
     }
 
     if ($this->characterEncoding) {
       $skip_cache = true;
     }
 
     if ($this->highlightAs) {
       $skip_cache = true;
     }
 
     $this->whitespaceMode = $whitespace_mode;
 
     $changeset = $this->changeset;
 
     if ($changeset->getFileType() != DifferentialChangeType::FILE_TEXT &&
         $changeset->getFileType() != DifferentialChangeType::FILE_SYMLINK) {
 
       $this->markGenerated();
 
     } else {
       if ($skip_cache || !$this->loadCache()) {
         $this->process();
         if (!$skip_cache) {
           $this->saveCache();
         }
       }
     }
   }
 
   private function process() {
     $whitespace_mode = $this->whitespaceMode;
     $changeset = $this->changeset;
 
     $ignore_all = (($whitespace_mode == self::WHITESPACE_IGNORE_MOST) ||
                   ($whitespace_mode == self::WHITESPACE_IGNORE_ALL));
 
     $force_ignore = ($whitespace_mode == self::WHITESPACE_IGNORE_ALL);
 
     if (!$force_ignore) {
       if ($ignore_all && $changeset->getWhitespaceMatters()) {
         $ignore_all = false;
       }
     }
 
     // The "ignore all whitespace" algorithm depends on rediffing the
     // files, and we currently need complete representations of both
     // files to do anything reasonable. If we only have parts of the files,
     // don't use the "ignore all" algorithm.
     if ($ignore_all) {
       $hunks = $changeset->getHunks();
       if (count($hunks) !== 1) {
         $ignore_all = false;
       } else {
         $first_hunk = reset($hunks);
         if ($first_hunk->getOldOffset() != 1 ||
             $first_hunk->getNewOffset() != 1) {
             $ignore_all = false;
         }
       }
     }
 
     if ($ignore_all) {
       $old_file = $changeset->makeOldFile();
       $new_file = $changeset->makeNewFile();
       if ($old_file == $new_file) {
         // If the old and new files are exactly identical, the synthetic
         // diff below will give us nonsense and whitespace modes are
         // irrelevant anyway. This occurs when you, e.g., copy a file onto
         // itself in Subversion (see T271).
         $ignore_all = false;
       }
     }
 
     $hunk_parser = new DifferentialHunkParser();
     $hunk_parser->setWhitespaceMode($whitespace_mode);
     $hunk_parser->parseHunksForLineData($changeset->getHunks());
 
     // Depending on the whitespace mode, we may need to compute a different
     // set of changes than the set of changes in the hunk data (specificaly,
     // we might want to consider changed lines which have only whitespace
     // changes as unchanged).
     if ($ignore_all) {
       $engine = new PhabricatorDifferenceEngine();
       $engine->setIgnoreWhitespace(true);
       $no_whitespace_changeset = $engine->generateChangesetFromFileContent(
         $old_file,
         $new_file);
 
       $type_parser = new DifferentialHunkParser();
       $type_parser->parseHunksForLineData($no_whitespace_changeset->getHunks());
 
       $hunk_parser->setOldLineTypeMap($type_parser->getOldLineTypeMap());
       $hunk_parser->setNewLineTypeMap($type_parser->getNewLineTypeMap());
     }
 
     $hunk_parser->reparseHunksForSpecialAttributes();
 
     $unchanged = false;
     if (!$hunk_parser->getHasAnyChanges()) {
       $filetype = $this->changeset->getFileType();
       if ($filetype == DifferentialChangeType::FILE_TEXT ||
           $filetype == DifferentialChangeType::FILE_SYMLINK) {
         $unchanged = true;
       }
     }
 
     $moveaway = false;
     $changetype = $this->changeset->getChangeType();
     if ($changetype == DifferentialChangeType::TYPE_MOVE_AWAY) {
       $moveaway = true;
     }
 
     $this->setSpecialAttributes(array(
       self::ATTR_UNCHANGED  => $unchanged,
       self::ATTR_DELETED    => $hunk_parser->getIsDeleted(),
       self::ATTR_WHITELINES => !$hunk_parser->getHasTextChanges(),
       self::ATTR_MOVEAWAY   => $moveaway,
     ));
 
     $hunk_parser->generateIntraLineDiffs();
     $hunk_parser->generateVisibileLinesMask();
 
     $this->setOldLines($hunk_parser->getOldLines());
     $this->setNewLines($hunk_parser->getNewLines());
     $this->setIntraLineDiffs($hunk_parser->getIntraLineDiffs());
     $this->setVisibileLinesMask($hunk_parser->getVisibleLinesMask());
     $this->hunkStartLines = $hunk_parser->getHunkStartLines(
       $changeset->getHunks());
 
     $new_corpus = $hunk_parser->getNewCorpus();
     $new_corpus_block = implode('', $new_corpus);
     $this->markGenerated($new_corpus_block);
 
     if ($this->isTopLevel &&
         !$this->comments &&
           ($this->isGenerated() ||
            $this->isUnchanged() ||
            $this->isDeleted())) {
       return;
     }
 
     $old_corpus = $hunk_parser->getOldCorpus();
     $old_corpus_block = implode('', $old_corpus);
     $old_future = $this->getHighlightFuture($old_corpus_block);
     $new_future = $this->getHighlightFuture($new_corpus_block);
     $futures = array(
       'old' => $old_future,
       'new' => $new_future,
     );
     $corpus_blocks = array(
       'old' => $old_corpus_block,
       'new' => $new_corpus_block,
     );
 
     $this->highlightErrors = false;
     foreach (new FutureIterator($futures) as $key => $future) {
       try {
         try {
           $highlighted = $future->resolve();
         } catch (PhutilSyntaxHighlighterException $ex) {
           $this->highlightErrors = true;
           $highlighted = id(new PhutilDefaultSyntaxHighlighter())
             ->getHighlightFuture($corpus_blocks[$key])
             ->resolve();
         }
         switch ($key) {
         case 'old':
           $this->oldRender = $this->processHighlightedSource(
             $this->old,
             $highlighted);
           break;
         case 'new':
           $this->newRender = $this->processHighlightedSource(
             $this->new,
             $highlighted);
           break;
         }
       } catch (Exception $ex) {
         phlog($ex);
         throw $ex;
       }
     }
 
     $this->applyIntraline(
       $this->oldRender,
       ipull($this->intra, 0),
       $old_corpus);
     $this->applyIntraline(
       $this->newRender,
       ipull($this->intra, 1),
       $new_corpus);
   }
 
   private function shouldRenderPropertyChangeHeader($changeset) {
     if (!$this->isTopLevel) {
       // We render properties only at top level; otherwise we get multiple
       // copies of them when a user clicks "Show More".
       return false;
     }
 
     return true;
   }
 
   public function render(
     $range_start  = null,
     $range_len    = null,
     $mask_force   = array()) {
 
     // "Top level" renders are initial requests for the whole file, versus
     // requests for a specific range generated by clicking "show more". We
     // generate property changes and "shield" UI elements only for toplevel
     // requests.
     $this->isTopLevel = (($range_start === null) && ($range_len === null));
     $this->highlightEngine = PhabricatorSyntaxHighlighter::newEngine();
 
     $encoding = null;
     if ($this->characterEncoding) {
       // We are forcing this changeset to be interpreted with a specific
       // character encoding, so force all the hunks into that encoding and
       // propagate it to the renderer.
       $encoding = $this->characterEncoding;
       foreach ($this->changeset->getHunks() as $hunk) {
         $hunk->forceEncoding($this->characterEncoding);
       }
     } else {
       // We're just using the default, so tell the renderer what that is
       // (by reading the encoding from the first hunk).
       foreach ($this->changeset->getHunks() as $hunk) {
         $encoding = $hunk->getDataEncoding();
         break;
       }
     }
 
     $this->tryCacheStuff();
     $render_pch = $this->shouldRenderPropertyChangeHeader($this->changeset);
 
     $rows = max(
       count($this->old),
       count($this->new));
 
     $renderer = $this->getRenderer()
       ->setUser($this->getUser())
       ->setChangeset($this->changeset)
       ->setRenderPropertyChangeHeader($render_pch)
       ->setIsTopLevel($this->isTopLevel)
       ->setOldRender($this->oldRender)
       ->setNewRender($this->newRender)
       ->setHunkStartLines($this->hunkStartLines)
       ->setOldChangesetID($this->leftSideChangesetID)
       ->setNewChangesetID($this->rightSideChangesetID)
       ->setOldAttachesToNewFile($this->leftSideAttachesToNewFile)
       ->setNewAttachesToNewFile($this->rightSideAttachesToNewFile)
       ->setCodeCoverage($this->getCoverage())
       ->setRenderingReference($this->getRenderingReference())
       ->setMarkupEngine($this->markupEngine)
       ->setHandles($this->handles)
       ->setOldLines($this->old)
       ->setNewLines($this->new)
       ->setOriginalCharacterEncoding($encoding)
       ->setShowEditAndReplyLinks($this->getShowEditAndReplyLinks())
       ->setCanMarkDone($this->getCanMarkDone())
       ->setObjectOwnerPHID($this->getObjectOwnerPHID())
       ->setHighlightingDisabled($this->highlightingDisabled);
 
     $shield = null;
     if ($this->isTopLevel && !$this->comments) {
       if ($this->isGenerated()) {
         $shield = $renderer->renderShield(
           pht(
             'This file contains generated code, which does not normally '.
             'need to be reviewed.'));
       } else if ($this->isMoveAway()) {
         // We put an empty shield on these files. Normally, they do not have
         // any diff content anyway. However, if they come through `arc`, they
         // may have content. We don't want to show it (it's not useful) and
         // we bailed out of fully processing it earlier anyway.
 
         // We could show a message like "this file was moved", but we show
         // that as a change header anyway, so it would be redundant. Instead,
         // just render an empty shield to skip rendering the diff body.
         $shield = '';
       } else if ($this->isUnchanged()) {
         $type = 'text';
         if (!$rows) {
           // NOTE: Normally, diffs which don't change files do not include
           // file content (for example, if you "chmod +x" a file and then
           // run "git show", the file content is not available). Similarly,
           // if you move a file from A to B without changing it, diffs normally
           // do not show the file content. In some cases `arc` is able to
           // synthetically generate content for these diffs, but for raw diffs
           // we'll never have it so we need to be prepared to not render a link.
           $type = 'none';
         }
 
         $type_add = DifferentialChangeType::TYPE_ADD;
         if ($this->changeset->getChangeType() == $type_add) {
           // Although the generic message is sort of accurate in a technical
           // sense, this more-tailored message is less confusing.
           $shield = $renderer->renderShield(
             pht('This is an empty file.'),
             $type);
         } else {
           $shield = $renderer->renderShield(
             pht('The contents of this file were not changed.'),
             $type);
         }
       } else if ($this->isWhitespaceOnly()) {
         $shield = $renderer->renderShield(
           pht('This file was changed only by adding or removing whitespace.'),
           'whitespace');
       } else if ($this->isDeleted()) {
         $shield = $renderer->renderShield(
           pht('This file was completely deleted.'));
       } else if ($this->changeset->getAffectedLineCount() > 2500) {
         $shield = $renderer->renderShield(
           pht(
             'This file has a very large number of changes (%s lines).',
             new PhutilNumber($this->changeset->getAffectedLineCount())));
       }
     }
 
     if ($shield !== null) {
       return $renderer->renderChangesetTable($shield);
     }
 
     // This request should render the "undershield" headers if it's a top-level
     // request which made it this far (indicating the changeset has no shield)
     // or it's a request with no mask information (indicating it's the request
     // that removes the rendering shield). Possibly, this second class of
     // request might need to be made more explicit.
     $is_undershield = (empty($mask_force) || $this->isTopLevel);
     $renderer->setIsUndershield($is_undershield);
 
     $old_comments = array();
     $new_comments = array();
     $old_mask = array();
     $new_mask = array();
     $feedback_mask = array();
 
     if ($this->comments) {
       // If there are any comments which appear in sections of the file which
       // we don't have, we're going to move them backwards to the closest
       // earlier line. Two cases where this may happen are:
       //
       //   - Porting ghost comments forward into a file which was mostly
       //     deleted.
       //   - Porting ghost comments forward from a full-context diff to a
       //     partial-context diff.
 
       list($old_backmap, $new_backmap) = $this->buildLineBackmaps();
 
       foreach ($this->comments as $comment) {
         $new_side = $this->isCommentOnRightSideWhenDisplayed($comment);
 
         $line = $comment->getLineNumber();
         if ($new_side) {
           $back_line = $new_backmap[$line];
         } else {
           $back_line = $old_backmap[$line];
         }
 
         if ($back_line != $line) {
           // TODO: This should probably be cleaner, but just be simple and
           // obvious for now.
           $ghost = $comment->getIsGhost();
           if ($ghost) {
             $moved = pht(
               'This comment originally appeared on line %s, but that line '.
               'does not exist in this version of the diff. It has been '.
               'moved backward to the nearest line.',
               new PhutilNumber($line));
             $ghost['reason'] = $ghost['reason']."\n\n".$moved;
             $comment->setIsGhost($ghost);
           }
 
           $comment->setLineNumber($back_line);
           $comment->setLineLength(0);
 
         }
 
         $start = max($comment->getLineNumber() - self::LINES_CONTEXT, 0);
         $end = $comment->getLineNumber() +
           $comment->getLineLength() +
           self::LINES_CONTEXT;
         for ($ii = $start; $ii <= $end; $ii++) {
           if ($new_side) {
             $new_mask[$ii] = true;
           } else {
             $old_mask[$ii] = true;
           }
         }
       }
 
       foreach ($this->old as $ii => $old) {
         if (isset($old['line']) && isset($old_mask[$old['line']])) {
           $feedback_mask[$ii] = true;
         }
       }
 
       foreach ($this->new as $ii => $new) {
         if (isset($new['line']) && isset($new_mask[$new['line']])) {
           $feedback_mask[$ii] = true;
         }
       }
 
       $this->comments = msort($this->comments, 'getID');
       foreach ($this->comments as $comment) {
         $final = $comment->getLineNumber() +
           $comment->getLineLength();
         $final = max(1, $final);
         if ($this->isCommentOnRightSideWhenDisplayed($comment)) {
           $new_comments[$final][] = $comment;
         } else {
           $old_comments[$final][] = $comment;
         }
       }
     }
     $renderer
       ->setOldComments($old_comments)
       ->setNewComments($new_comments);
 
     switch ($this->changeset->getFileType()) {
       case DifferentialChangeType::FILE_IMAGE:
         $old = null;
         $new = null;
         // TODO: Improve the architectural issue as discussed in D955
         // https://secure.phabricator.com/D955
         $reference = $this->getRenderingReference();
         $parts = explode('/', $reference);
         if (count($parts) == 2) {
           list($id, $vs) = $parts;
         } else {
           $id = $parts[0];
           $vs = 0;
         }
         $id = (int)$id;
         $vs = (int)$vs;
 
         if (!$vs) {
           $metadata = $this->changeset->getMetadata();
           $data = idx($metadata, 'attachment-data');
 
           $old_phid = idx($metadata, 'old:binary-phid');
           $new_phid = idx($metadata, 'new:binary-phid');
         } else {
           $vs_changeset = id(new DifferentialChangeset())->load($vs);
           $old_phid = null;
           $new_phid = null;
 
           // TODO: This is spooky, see D6851
           if ($vs_changeset) {
             $vs_metadata = $vs_changeset->getMetadata();
             $old_phid = idx($vs_metadata, 'new:binary-phid');
           }
 
           $changeset = id(new DifferentialChangeset())->load($id);
           if ($changeset) {
             $metadata = $changeset->getMetadata();
             $new_phid = idx($metadata, 'new:binary-phid');
           }
         }
 
         if ($old_phid || $new_phid) {
           // grab the files, (micro) optimization for 1 query not 2
           $file_phids = array();
           if ($old_phid) {
             $file_phids[] = $old_phid;
           }
           if ($new_phid) {
             $file_phids[] = $new_phid;
           }
 
           $files = id(new PhabricatorFileQuery())
             ->setViewer($this->getUser())
             ->withPHIDs($file_phids)
             ->execute();
           foreach ($files as $file) {
             if (empty($file)) {
               continue;
             }
             if ($file->getPHID() == $old_phid) {
               $old = $file;
             } else if ($file->getPHID() == $new_phid) {
               $new = $file;
             }
           }
         }
 
         $renderer->attachOldFile($old);
         $renderer->attachNewFile($new);
 
         return $renderer->renderFileChange($old, $new, $id, $vs);
       case DifferentialChangeType::FILE_DIRECTORY:
       case DifferentialChangeType::FILE_BINARY:
         $output = $renderer->renderChangesetTable(null);
         return $output;
     }
 
     if ($this->originalLeft && $this->originalRight) {
       list($highlight_old, $highlight_new) = $this->diffOriginals();
       $highlight_old = array_flip($highlight_old);
       $highlight_new = array_flip($highlight_new);
       $renderer
         ->setHighlightOld($highlight_old)
         ->setHighlightNew($highlight_new);
     }
     $renderer
       ->setOriginalOld($this->originalLeft)
       ->setOriginalNew($this->originalRight);
 
     if ($range_start === null) {
       $range_start = 0;
     }
     if ($range_len === null) {
       $range_len = $rows;
     }
     $range_len = min($range_len, $rows - $range_start);
 
     list($gaps, $mask, $depths) = $this->calculateGapsMaskAndDepths(
       $mask_force,
       $feedback_mask,
       $range_start,
       $range_len);
 
     $renderer
       ->setGaps($gaps)
       ->setMask($mask)
       ->setDepths($depths);
 
     $html = $renderer->renderTextChange(
       $range_start,
       $range_len,
       $rows);
 
     return $renderer->renderChangesetTable($html);
   }
 
   /**
    * This function calculates a lot of stuff we need to know to display
    * the diff:
    *
    * Gaps - compute gaps in the visible display diff, where we will render
    * "Show more context" spacers. If a gap is smaller than the context size,
    * we just display it. Otherwise, we record it into $gaps and will render a
    * "show more context" element instead of diff text below. A given $gap
    * is a tuple of $gap_line_number_start and $gap_length.
    *
    * Mask - compute the actual lines that need to be shown (because they
    * are near changes lines, near inline comments, or the request has
    * explicitly asked for them, i.e. resulting from the user clicking
    * "show more"). The $mask returned is a sparesely populated dictionary
    * of $visible_line_number => true.
    *
    * Depths - compute how indented any given line is. The $depths returned
    * is a sparesely populated dictionary of $visible_line_number => $depth.
    *
    * This function also has the side effect of modifying member variable
    * new such that tabs are normalized to spaces for each line of the diff.
    *
    * @return array($gaps, $mask, $depths)
    */
   private function calculateGapsMaskAndDepths(
     $mask_force,
     $feedback_mask,
     $range_start,
     $range_len) {
 
     // Calculate gaps and mask first
     $gaps = array();
     $gap_start = 0;
     $in_gap = false;
     $base_mask = $this->visible + $mask_force + $feedback_mask;
     $base_mask[$range_start + $range_len] = true;
     for ($ii = $range_start; $ii <= $range_start + $range_len; $ii++) {
       if (isset($base_mask[$ii])) {
         if ($in_gap) {
           $gap_length = $ii - $gap_start;
           if ($gap_length <= self::LINES_CONTEXT) {
             for ($jj = $gap_start; $jj <= $gap_start + $gap_length; $jj++) {
               $base_mask[$jj] = true;
             }
           } else {
             $gaps[] = array($gap_start, $gap_length);
           }
           $in_gap = false;
         }
       } else {
         if (!$in_gap) {
           $gap_start = $ii;
           $in_gap = true;
         }
       }
     }
     $gaps = array_reverse($gaps);
     $mask = $base_mask;
 
     // Time to calculate depth.
     // We need to go backwards to properly indent whitespace in this code:
     //
     //   0: class C {
     //   1:
     //   1:   function f() {
     //   2:
     //   2:     return;
     //   1:
     //   1:   }
     //   0:
     //   0: }
     //
     $depths = array();
     $last_depth = 0;
     $range_end = $range_start + $range_len;
     if (!isset($this->new[$range_end])) {
       $range_end--;
     }
     for ($ii = $range_end; $ii >= $range_start; $ii--) {
       // We need to expand tabs to process mixed indenting and to round
       // correctly later.
       $line = str_replace("\t", '  ', $this->new[$ii]['text']);
       $trimmed = ltrim($line);
       if ($trimmed != '') {
         // We round down to flatten "/**" and " *".
         $last_depth = floor((strlen($line) - strlen($trimmed)) / 2);
       }
       $depths[$ii] = $last_depth;
     }
 
     return array($gaps, $mask, $depths);
   }
 
   /**
    * Determine if an inline comment will appear on the rendered diff,
    * taking into consideration which halves of which changesets will actually
    * be shown.
    *
    * @param PhabricatorInlineCommentInterface Comment to test for visibility.
    * @return bool True if the comment is visible on the rendered diff.
    */
   private function isCommentVisibleOnRenderedDiff(
     PhabricatorInlineCommentInterface $comment) {
 
     $changeset_id = $comment->getChangesetID();
     $is_new = $comment->getIsNewFile();
 
     if ($changeset_id == $this->rightSideChangesetID &&
         $is_new == $this->rightSideAttachesToNewFile) {
         return true;
     }
 
     if ($changeset_id == $this->leftSideChangesetID &&
         $is_new == $this->leftSideAttachesToNewFile) {
         return true;
     }
 
     return false;
   }
 
 
   /**
    * Determine if a comment will appear on the right side of the display diff.
    * Note that the comment must appear somewhere on the rendered changeset, as
    * per isCommentVisibleOnRenderedDiff().
    *
    * @param PhabricatorInlineCommentInterface Comment to test for display
    *              location.
    * @return bool True for right, false for left.
    */
   private function isCommentOnRightSideWhenDisplayed(
     PhabricatorInlineCommentInterface $comment) {
 
     if (!$this->isCommentVisibleOnRenderedDiff($comment)) {
       throw new Exception(pht('Comment is not visible on changeset!'));
     }
 
     $changeset_id = $comment->getChangesetID();
     $is_new = $comment->getIsNewFile();
 
     if ($changeset_id == $this->rightSideChangesetID &&
         $is_new == $this->rightSideAttachesToNewFile) {
       return true;
     }
 
     return false;
   }
 
   /**
    * Parse the 'range' specification that this class and the client-side JS
    * emit to indicate that a user clicked "Show more..." on a diff. Generally,
    * use is something like this:
    *
    *   $spec = $request->getStr('range');
    *   $parsed = DifferentialChangesetParser::parseRangeSpecification($spec);
    *   list($start, $end, $mask) = $parsed;
    *   $parser->render($start, $end, $mask);
    *
    * @param string Range specification, indicating the range of the diff that
    *               should be rendered.
    * @return tuple List of <start, end, mask> suitable for passing to
    *               @{method:render}.
    */
   public static function parseRangeSpecification($spec) {
     $range_s = null;
     $range_e = null;
     $mask = array();
 
     if ($spec) {
       $match = null;
       if (preg_match('@^(\d+)-(\d+)(?:/(\d+)-(\d+))?$@', $spec, $match)) {
         $range_s = (int)$match[1];
         $range_e = (int)$match[2];
         if (count($match) > 3) {
           $start = (int)$match[3];
           $len = (int)$match[4];
           for ($ii = $start; $ii < $start + $len; $ii++) {
             $mask[$ii] = true;
           }
         }
       }
     }
 
     return array($range_s, $range_e, $mask);
   }
 
   /**
    * Render "modified coverage" information; test coverage on modified lines.
    * This synthesizes diff information with unit test information into a useful
    * indicator of how well tested a change is.
    */
   public function renderModifiedCoverage() {
     $na = phutil_tag('em', array(), '-');
 
     $coverage = $this->getCoverage();
     if (!$coverage) {
       return $na;
     }
 
     $covered = 0;
     $not_covered = 0;
 
     foreach ($this->new as $k => $new) {
       if (!$new['line']) {
         continue;
       }
 
       if (!$new['type']) {
         continue;
       }
 
       if (empty($coverage[$new['line'] - 1])) {
         continue;
       }
 
       switch ($coverage[$new['line'] - 1]) {
         case 'C':
           $covered++;
           break;
         case 'U':
           $not_covered++;
           break;
       }
     }
 
     if (!$covered && !$not_covered) {
       return $na;
     }
 
     return sprintf('%d%%', 100 * ($covered / ($covered + $not_covered)));
   }
 
   public function detectCopiedCode(
     array $changesets,
     $min_width = 30,
     $min_lines = 3) {
 
     assert_instances_of($changesets, 'DifferentialChangeset');
 
     $map = array();
     $files = array();
     $types = array();
     foreach ($changesets as $changeset) {
       $file = $changeset->getFilename();
       foreach ($changeset->getHunks() as $hunk) {
         $lines = $hunk->getStructuredOldFile();
         foreach ($lines as $line => $info) {
           $type = $info['type'];
           if ($type == '\\') {
             continue;
           }
           $types[$file][$line] = $type;
 
           $text = $info['text'];
           $text = trim($text);
           $files[$file][$line] = $text;
 
           if (strlen($text) >= $min_width) {
             $map[$text][] = array($file, $line);
           }
         }
       }
     }
 
     foreach ($changesets as $changeset) {
       $copies = array();
       foreach ($changeset->getHunks() as $hunk) {
         $added = $hunk->getStructuredNewFile();
         $atype = array();
 
         foreach ($added as $line => $info) {
           $atype[$line] = $info['type'];
           $added[$line] = trim($info['text']);
         }
 
         $skip_lines = 0;
         foreach ($added as $line => $code) {
           if ($skip_lines) {
             // We're skipping lines that we already processed because we
             // extended a block above them downward to include them.
             $skip_lines--;
             continue;
           }
 
           if ($atype[$line] !== '+') {
             // This line hasn't been changed in the new file, so don't try
             // to figure out where it came from.
             continue;
           }
 
           if (empty($map[$code])) {
             // This line was too short to trigger copy/move detection.
             continue;
           }
 
           if (count($map[$code]) > 16) {
             // If there are a large number of identical lines in this diff,
             // don't try to figure out where this block came from: the analysis
             // is O(N^2), since we need to compare every line against every
             // other line. Even if we arrive at a result, it is unlikely to be
             // meaningful. See T5041.
             continue;
           }
 
           $best_length = 0;
 
           // Explore all candidates.
           foreach ($map[$code] as $val) {
             list($file, $orig_line) = $val;
             $length = 1;
 
             // Search backward and forward to find all of the adjacent lines
             // which match.
             foreach (array(-1, 1) as $direction) {
               $offset = $direction;
               while (true) {
                 if (isset($copies[$line + $offset])) {
                   // If we run into a block above us which we've already
                   // attributed to a move or copy from elsewhere, stop
                   // looking.
                   break;
                 }
 
                 if (!isset($added[$line + $offset])) {
                   // If we've run off the beginning or end of the new file,
                   // stop looking.
                   break;
                 }
 
                 if (!isset($files[$file][$orig_line + $offset])) {
                   // If we've run off the beginning or end of the original
                   // file, we also stop looking.
                   break;
                 }
 
                 $old = $files[$file][$orig_line + $offset];
                 $new = $added[$line + $offset];
                 if ($old !== $new) {
                   // If the old line doesn't match the new line, stop
                   // looking.
                   break;
                 }
 
                 $length++;
                 $offset += $direction;
               }
             }
 
             if ($length < $best_length) {
               // If we already know of a better source (more matching lines)
               // for this move/copy, stick with that one. We prefer long
               // copies/moves which match a lot of context over short ones.
               continue;
             }
 
             if ($length == $best_length) {
               if (idx($types[$file], $orig_line) != '-') {
                 // If we already know of an equally good source (same number
                 // of matching lines) and this isn't a move, stick with the
                 // other one. We prefer moves over copies.
                 continue;
               }
             }
 
             $best_length = $length;
             // ($offset - 1) contains number of forward matching lines.
             $best_offset = $offset - 1;
             $best_file = $file;
             $best_line = $orig_line;
           }
 
           $file = ($best_file == $changeset->getFilename() ? '' : $best_file);
           for ($i = $best_length; $i--; ) {
             $type = idx($types[$best_file], $best_line + $best_offset - $i);
             $copies[$line + $best_offset - $i] = ($best_length < $min_lines
               ? array() // Ignore short blocks.
               : array($file, $best_line + $best_offset - $i, $type));
           }
 
           $skip_lines = $best_offset;
         }
       }
 
       $copies = array_filter($copies);
       if ($copies) {
         $metadata = $changeset->getMetadata();
         $metadata['copy:lines'] = $copies;
         $changeset->setMetadata($metadata);
       }
     }
     return $changesets;
   }
 
   /**
    * Build maps from lines comments appear on to actual lines.
    */
   private function buildLineBackmaps() {
     $old_back = array();
     $new_back = array();
     foreach ($this->old as $ii => $old) {
       $old_back[$old['line']] = $old['line'];
     }
     foreach ($this->new as $ii => $new) {
       $new_back[$new['line']] = $new['line'];
     }
 
     $max_old_line = 0;
     $max_new_line = 0;
     foreach ($this->comments as $comment) {
       if ($this->isCommentOnRightSideWhenDisplayed($comment)) {
         $max_new_line = max($max_new_line, $comment->getLineNumber());
       } else {
         $max_old_line = max($max_old_line, $comment->getLineNumber());
       }
     }
 
     $cursor = 1;
     for ($ii = 1; $ii <= $max_old_line; $ii++) {
       if (empty($old_back[$ii])) {
         $old_back[$ii] = $cursor;
       } else {
         $cursor = $old_back[$ii];
       }
     }
 
     $cursor = 1;
     for ($ii = 1; $ii <= $max_new_line; $ii++) {
       if (empty($new_back[$ii])) {
         $new_back[$ii] = $cursor;
       } else {
         $cursor = $new_back[$ii];
       }
     }
 
     return array($old_back, $new_back);
   }
 
 }
diff --git a/src/applications/differential/parser/DifferentialCommitMessageParser.php b/src/applications/differential/parser/DifferentialCommitMessageParser.php
index 00d2fe2c3..fda7ae05b 100644
--- a/src/applications/differential/parser/DifferentialCommitMessageParser.php
+++ b/src/applications/differential/parser/DifferentialCommitMessageParser.php
@@ -1,216 +1,216 @@
 <?php
 
 /**
  * Parses commit messages (containing relatively freeform text with textual
  * field labels) into a dictionary of fields.
  *
  *   $parser = id(new DifferentialCommitMessageParser())
  *     ->setLabelMap($label_map)
  *     ->setTitleKey($key_title)
  *     ->setSummaryKey($key_summary);
  *
  *   $fields = $parser->parseCorpus($corpus);
  *   $errors = $parser->getErrors();
  *
  * This is used by Differential to parse messages entered from the command line.
  *
  * @task config   Configuring the Parser
  * @task parse    Parsing Messages
  * @task support  Support Methods
  * @task internal Internals
  */
-final class DifferentialCommitMessageParser {
+final class DifferentialCommitMessageParser extends Phobject {
 
   private $labelMap;
   private $titleKey;
   private $summaryKey;
   private $errors;
 
 
 /* -(  Configuring the Parser  )--------------------------------------------- */
 
 
   /**
    * @task config
    */
   public function setLabelMap(array $label_map) {
     $this->labelMap = $label_map;
     return $this;
   }
 
 
   /**
    * @task config
    */
   public function setTitleKey($title_key) {
     $this->titleKey = $title_key;
     return $this;
   }
 
 
   /**
    * @task config
    */
   public function setSummaryKey($summary_key) {
     $this->summaryKey = $summary_key;
     return $this;
   }
 
 
 /* -(  Parsing Messages  )--------------------------------------------------- */
 
 
   /**
    * @task parse
    */
   public function parseCorpus($corpus) {
     $this->errors = array();
 
     $label_map = $this->labelMap;
     $key_title = $this->titleKey;
     $key_summary = $this->summaryKey;
 
     if (!$key_title || !$key_summary || ($label_map === null)) {
       throw new Exception(
         pht(
           'Expected %s, %s and %s to be set before parsing a corpus.',
           'labelMap',
           'summaryKey',
           'titleKey'));
     }
 
     $label_regexp = $this->buildLabelRegexp($label_map);
 
     // NOTE: We're special casing things here to make the "Title:" label
     // optional in the message.
     $field = $key_title;
 
     $seen = array();
     $lines = explode("\n", trim($corpus));
     $field_map = array();
     foreach ($lines as $key => $line) {
       $match = null;
       if (preg_match($label_regexp, $line, $match)) {
         $lines[$key] = trim($match['text']);
         $field = $label_map[self::normalizeFieldLabel($match['field'])];
         if (!empty($seen[$field])) {
           $this->errors[] = pht(
             'Field "%s" occurs twice in commit message!',
             $field);
         }
         $seen[$field] = true;
       }
       $field_map[$key] = $field;
     }
 
     $fields = array();
     foreach ($lines as $key => $line) {
       $fields[$field_map[$key]][] = $line;
     }
 
     // This is a piece of special-cased magic which allows you to omit the
     // field labels for "title" and "summary". If the user enters a large block
     // of text at the beginning of the commit message with an empty line in it,
     // treat everything before the blank line as "title" and everything after
     // as "summary".
     if (isset($fields[$key_title]) && empty($fields[$key_summary])) {
       $lines = $fields[$key_title];
       for ($ii = 0; $ii < count($lines); $ii++) {
         if (strlen(trim($lines[$ii])) == 0) {
           break;
         }
       }
       if ($ii != count($lines)) {
         $fields[$key_title] = array_slice($lines, 0, $ii);
         $summary = array_slice($lines, $ii);
         if (strlen(trim(implode("\n", $summary)))) {
           $fields[$key_summary] = $summary;
         }
       }
     }
 
     // Implode all the lines back into chunks of text.
     foreach ($fields as $name => $lines) {
       $data = rtrim(implode("\n", $lines));
       $data = ltrim($data, "\n");
       $fields[$name] = $data;
     }
 
     // This is another piece of special-cased magic which allows you to
     // enter a ridiculously long title, or just type a big block of stream
     // of consciousness text, and have some sort of reasonable result conjured
     // from it.
     if (isset($fields[$key_title])) {
       $terminal = '...';
       $title = $fields[$key_title];
       $short = id(new PhutilUTF8StringTruncator())
         ->setMaximumBytes(250)
         ->setTerminator($terminal)
         ->truncateString($title);
 
       if ($short != $title) {
 
         // If we shortened the title, split the rest into the summary, so
         // we end up with a title like:
         //
         //    Title title tile title title...
         //
         // ...and a summary like:
         //
         //    ...title title title.
         //
         //    Summary summary summary summary.
 
         $summary = idx($fields, $key_summary, '');
         $offset = strlen($short) - strlen($terminal);
         $remainder = ltrim(substr($fields[$key_title], $offset));
         $summary = '...'.$remainder."\n\n".$summary;
         $summary = rtrim($summary, "\n");
 
         $fields[$key_title] = $short;
         $fields[$key_summary] = $summary;
       }
     }
 
     return $fields;
   }
 
 
   /**
    * @task parse
    */
   public function getErrors() {
     return $this->errors;
   }
 
 
 /* -(  Support Methods  )---------------------------------------------------- */
 
 
   /**
    * @task support
    */
   public static function normalizeFieldLabel($label) {
     return phutil_utf8_strtolower($label);
   }
 
 
 /* -(  Internals  )---------------------------------------------------------- */
 
 
   /**
    * @task internal
    */
   private function buildLabelRegexp(array $label_map) {
     $field_labels = array_keys($label_map);
     foreach ($field_labels as $key => $label) {
       $field_labels[$key] = preg_quote($label, '/');
     }
     $field_labels = implode('|', $field_labels);
 
     $field_pattern = '/^(?P<field>'.$field_labels.'):(?P<text>.*)$/i';
 
     return $field_pattern;
   }
 
 }
diff --git a/src/applications/differential/parser/DifferentialHunkParser.php b/src/applications/differential/parser/DifferentialHunkParser.php
index d49f4e1e2..8903c85f9 100644
--- a/src/applications/differential/parser/DifferentialHunkParser.php
+++ b/src/applications/differential/parser/DifferentialHunkParser.php
@@ -1,675 +1,675 @@
 <?php
 
-final class DifferentialHunkParser {
+final class DifferentialHunkParser extends Phobject {
 
   private $oldLines;
   private $newLines;
   private $intraLineDiffs;
   private $visibleLinesMask;
   private $whitespaceMode;
 
   /**
    * Get a map of lines on which hunks start, other than line 1. This
    * datastructure is used to determine when to render "Context not available."
    * in diffs with multiple hunks.
    *
    * @return dict<int, bool>  Map of lines where hunks start, other than line 1.
    */
   public function getHunkStartLines(array $hunks) {
     assert_instances_of($hunks, 'DifferentialHunk');
 
     $map = array();
     foreach ($hunks as $hunk) {
       $line = $hunk->getOldOffset();
       if ($line > 1) {
         $map[$line] = true;
       }
     }
 
     return $map;
   }
 
   private function setVisibleLinesMask($mask) {
     $this->visibleLinesMask = $mask;
     return $this;
   }
   public function getVisibleLinesMask() {
     if ($this->visibleLinesMask === null) {
       throw new PhutilInvalidStateException('generateVisibileLinesMask');
     }
     return $this->visibleLinesMask;
   }
 
   private function setIntraLineDiffs($intra_line_diffs) {
     $this->intraLineDiffs = $intra_line_diffs;
     return $this;
   }
   public function getIntraLineDiffs() {
     if ($this->intraLineDiffs === null) {
       throw new PhutilInvalidStateException('generateIntraLineDiffs');
     }
     return $this->intraLineDiffs;
   }
 
   private function setNewLines($new_lines) {
     $this->newLines = $new_lines;
     return $this;
   }
   public function getNewLines() {
     if ($this->newLines === null) {
       throw new PhutilInvalidStateException('parseHunksForLineData');
     }
     return $this->newLines;
   }
 
   private function setOldLines($old_lines) {
     $this->oldLines = $old_lines;
     return $this;
   }
   public function getOldLines() {
     if ($this->oldLines === null) {
       throw new PhutilInvalidStateException('parseHunksForLineData');
     }
     return $this->oldLines;
   }
 
   public function getOldLineTypeMap() {
     $map = array();
     $old = $this->getOldLines();
     foreach ($old as $o) {
       if (!$o) {
         continue;
       }
       $map[$o['line']] = $o['type'];
     }
     return $map;
   }
 
   public function setOldLineTypeMap(array $map) {
     $lines = $this->getOldLines();
     foreach ($lines as $key => $data) {
       $lines[$key]['type'] = idx($map, $data['line']);
     }
     $this->oldLines = $lines;
     return $this;
   }
 
   public function getNewLineTypeMap() {
     $map = array();
     $new = $this->getNewLines();
     foreach ($new as $n) {
       if (!$n) {
         continue;
       }
       $map[$n['line']] = $n['type'];
     }
     return $map;
   }
 
   public function setNewLineTypeMap(array $map) {
     $lines = $this->getNewLines();
     foreach ($lines as $key => $data) {
       $lines[$key]['type'] = idx($map, $data['line']);
     }
     $this->newLines = $lines;
     return $this;
   }
 
 
   public function setWhitespaceMode($white_space_mode) {
     $this->whitespaceMode = $white_space_mode;
     return $this;
   }
 
   private function getWhitespaceMode() {
     if ($this->whitespaceMode === null) {
       throw new Exception(
         pht(
           'You must %s before accessing this data.',
           'setWhitespaceMode'));
     }
     return $this->whitespaceMode;
   }
 
   public function getIsDeleted() {
     foreach ($this->getNewLines() as $line) {
       if ($line) {
         // At least one new line, so the entire file wasn't deleted.
         return false;
       }
     }
 
     foreach ($this->getOldLines() as $line) {
       if ($line) {
         // No new lines, at least one old line; the entire file was deleted.
         return true;
       }
     }
 
     // This is an empty file.
     return false;
   }
 
   /**
    * Returns true if the hunks change any text, not just whitespace.
    */
   public function getHasTextChanges() {
     return $this->getHasChanges('text');
   }
 
   /**
    * Returns true if the hunks change anything, including whitespace.
    */
   public function getHasAnyChanges() {
     return $this->getHasChanges('any');
   }
 
   private function getHasChanges($filter) {
     if ($filter !== 'any' && $filter !== 'text') {
       throw new Exception(pht("Unknown change filter '%s'.", $filter));
     }
 
     $old = $this->getOldLines();
     $new = $this->getNewLines();
 
     $is_any = ($filter === 'any');
 
     foreach ($old as $key => $o) {
       $n = $new[$key];
       if ($o === null || $n === null) {
         // One side is missing, and it's impossible for both sides to be null,
         // so the other side must have something, and thus the two sides are
         // different and the file has been changed under any type of filter.
         return true;
       }
 
       if ($o['type'] !== $n['type']) {
         // The types are different, so either the underlying text is actually
         // different or whatever whitespace rules we're using consider them
         // different.
         return true;
       }
 
       if ($o['text'] !== $n['text']) {
         if ($is_any) {
           // The text is different, so there's a change.
           return true;
         } else if (trim($o['text']) !== trim($n['text'])) {
           return true;
         }
       }
     }
 
     // No changes anywhere in the file.
     return false;
   }
 
 
   /**
    * This function takes advantage of the parsing work done in
    * @{method:parseHunksForLineData} and continues the struggle to hammer this
    * data into something we can display to a user.
    *
    * In particular, this function re-parses the hunks to make them equivalent
    * in length for easy rendering, adding `null` as necessary to pad the
    * length.
    *
    * Anyhoo, this function is not particularly well-named but I try.
    *
    * NOTE: this function must be called after
    * @{method:parseHunksForLineData}.
    */
   public function reparseHunksForSpecialAttributes() {
     $rebuild_old = array();
     $rebuild_new = array();
 
     $old_lines = array_reverse($this->getOldLines());
     $new_lines = array_reverse($this->getNewLines());
 
     while (count($old_lines) || count($new_lines)) {
       $old_line_data = array_pop($old_lines);
       $new_line_data = array_pop($new_lines);
 
       if ($old_line_data) {
         $o_type = $old_line_data['type'];
       } else {
         $o_type = null;
       }
 
       if ($new_line_data) {
         $n_type = $new_line_data['type'];
       } else {
         $n_type = null;
       }
 
       // This line does not exist in the new file.
       if (($o_type != null) && ($n_type == null)) {
         $rebuild_old[] = $old_line_data;
         $rebuild_new[] = null;
         if ($new_line_data) {
           array_push($new_lines, $new_line_data);
         }
         continue;
       }
 
       // This line does not exist in the old file.
       if (($n_type != null) && ($o_type == null)) {
         $rebuild_old[] = null;
         $rebuild_new[] = $new_line_data;
         if ($old_line_data) {
           array_push($old_lines, $old_line_data);
         }
         continue;
       }
 
       $rebuild_old[] = $old_line_data;
       $rebuild_new[] = $new_line_data;
     }
 
     $this->setOldLines($rebuild_old);
     $this->setNewLines($rebuild_new);
 
     $this->updateChangeTypesForWhitespaceMode();
 
     return $this;
   }
 
   private function updateChangeTypesForWhitespaceMode() {
     $mode = $this->getWhitespaceMode();
 
     $mode_show_all = DifferentialChangesetParser::WHITESPACE_SHOW_ALL;
     if ($mode === $mode_show_all) {
       // If we're showing all whitespace, we don't need to perform any updates.
       return;
     }
 
     $mode_trailing = DifferentialChangesetParser::WHITESPACE_IGNORE_TRAILING;
     $is_trailing = ($mode === $mode_trailing);
 
     $new = $this->getNewLines();
     $old = $this->getOldLines();
     foreach ($old as $key => $o) {
       $n = $new[$key];
 
       if (!$o || !$n) {
         continue;
       }
 
       if ($is_trailing) {
         // In "trailing" mode, we need to identify lines which are marked
         // changed but differ only by trailing whitespace. We mark these lines
         // unchanged.
         if ($o['type'] != $n['type']) {
           if (rtrim($o['text']) === rtrim($n['text'])) {
             $old[$key]['type'] = null;
             $new[$key]['type'] = null;
           }
         }
       } else {
         // In "ignore most" and "ignore all" modes, we need to identify lines
         // which are marked unchanged but have internal whitespace changes.
         // We want to ignore leading and trailing whitespace changes only, not
         // internal whitespace changes (`diff` doesn't have a mode for this, so
         // we have to fix it here). If the text is marked unchanged but the
         // old and new text differs by internal space, mark the lines changed.
         if ($o['type'] === null && $n['type'] === null) {
           if ($o['text'] !== $n['text']) {
             if (trim($o['text']) !== trim($n['text'])) {
               $old[$key]['type'] = '-';
               $new[$key]['type'] = '+';
             }
           }
         }
       }
     }
 
     $this->setOldLines($old);
     $this->setNewLines($new);
 
     return $this;
   }
 
   public function generateIntraLineDiffs() {
     $old = $this->getOldLines();
     $new = $this->getNewLines();
 
     $diffs = array();
     foreach ($old as $key => $o) {
       $n = $new[$key];
 
       if (!$o || !$n) {
         continue;
       }
 
       if ($o['type'] != $n['type']) {
         $diffs[$key] = ArcanistDiffUtils::generateIntralineDiff(
           $o['text'],
           $n['text']);
       }
     }
 
     $this->setIntraLineDiffs($diffs);
 
     return $this;
   }
 
   public function generateVisibileLinesMask() {
     $lines_context = DifferentialChangesetParser::LINES_CONTEXT;
     $old = $this->getOldLines();
     $new = $this->getNewLines();
     $max_length = max(count($old), count($new));
     $visible = false;
     $last = 0;
     $mask = array();
     for ($cursor = -$lines_context; $cursor < $max_length; $cursor++) {
       $offset = $cursor + $lines_context;
       if ((isset($old[$offset]) && $old[$offset]['type']) ||
           (isset($new[$offset]) && $new[$offset]['type'])) {
         $visible = true;
         $last = $offset;
       } else if ($cursor > $last + $lines_context) {
         $visible = false;
       }
       if ($visible && $cursor > 0) {
         $mask[$cursor] = 1;
       }
     }
 
     $this->setVisibleLinesMask($mask);
 
     return $this;
   }
 
   public function getOldCorpus() {
     return $this->getCorpus($this->getOldLines());
   }
 
   public function getNewCorpus() {
     return $this->getCorpus($this->getNewLines());
   }
 
   private function getCorpus(array $lines) {
 
     $corpus = array();
     foreach ($lines as $l) {
       if ($l['type'] != '\\') {
         if ($l['text'] === null) {
           // There's no text on this side of the diff, but insert a placeholder
           // newline so the highlighted line numbers match up.
           $corpus[] = "\n";
         } else {
           $corpus[] = $l['text'];
         }
       }
     }
     return $corpus;
   }
 
   public function parseHunksForLineData(array $hunks) {
     assert_instances_of($hunks, 'DifferentialHunk');
 
     $old_lines = array();
     $new_lines = array();
     foreach ($hunks as $hunk) {
       $lines = $hunk->getSplitLines();
 
       $line_type_map = array();
       $line_text = array();
       foreach ($lines as $line_index => $line) {
         if (isset($line[0])) {
           $char = $line[0];
           switch ($char) {
             case ' ':
               $line_type_map[$line_index] = null;
               $line_text[$line_index] = substr($line, 1);
               break;
             case "\r":
             case "\n":
               // NOTE: Normally, the first character is a space, plus, minus or
               // backslash, but it may be a newline if it used to be a space and
               // trailing whitespace has been stripped via email transmission or
               // some similar mechanism. In these cases, we essentially pretend
               // the missing space is still there.
               $line_type_map[$line_index] = null;
               $line_text[$line_index] = $line;
               break;
             case '+':
             case '-':
             case '\\':
               $line_type_map[$line_index] = $char;
               $line_text[$line_index] = substr($line, 1);
               break;
             default:
               throw new Exception(
                 pht(
                   'Unexpected leading character "%s" at line index %s!',
                   $char,
                   $line_index));
           }
         } else {
           $line_type_map[$line_index] = null;
           $line_text[$line_index] = '';
         }
       }
 
       $old_line = $hunk->getOldOffset();
       $new_line = $hunk->getNewOffset();
 
       $num_lines = count($lines);
       for ($cursor = 0; $cursor < $num_lines; $cursor++) {
         $type = $line_type_map[$cursor];
         $data = array(
           'type'  => $type,
           'text'  => $line_text[$cursor],
           'line'  => $new_line,
         );
         if ($type == '\\') {
           $type = $line_type_map[$cursor - 1];
           $data['text'] = ltrim($data['text']);
         }
         switch ($type) {
           case '+':
             $new_lines[] = $data;
             ++$new_line;
             break;
           case '-':
             $data['line'] = $old_line;
             $old_lines[] = $data;
             ++$old_line;
             break;
           default:
             $new_lines[] = $data;
             $data['line'] = $old_line;
             $old_lines[] = $data;
             ++$new_line;
             ++$old_line;
             break;
         }
       }
     }
 
     $this->setOldLines($old_lines);
     $this->setNewLines($new_lines);
 
     return $this;
   }
 
   public function parseHunksForHighlightMasks(
     array $changeset_hunks,
     array $old_hunks,
     array $new_hunks) {
     assert_instances_of($changeset_hunks, 'DifferentialHunk');
     assert_instances_of($old_hunks,       'DifferentialHunk');
     assert_instances_of($new_hunks,       'DifferentialHunk');
 
     // Put changes side by side.
     $olds = array();
     $news = array();
     $olds_cursor = -1;
     $news_cursor = -1;
     foreach ($changeset_hunks as $hunk) {
       $n_old = $hunk->getOldOffset();
       $n_new = $hunk->getNewOffset();
       $changes = $hunk->getSplitLines();
       foreach ($changes as $line) {
         $diff_type = $line[0]; // Change type in diff of diffs.
         $orig_type = $line[1]; // Change type in the original diff.
         if ($diff_type == ' ') {
           // Use the same key for lines that are next to each other.
           if ($olds_cursor > $news_cursor) {
             $key = $olds_cursor + 1;
           } else {
             $key = $news_cursor + 1;
           }
           $olds[$key] = null;
           $news[$key] = null;
           $olds_cursor = $key;
           $news_cursor = $key;
         } else if ($diff_type == '-') {
           $olds[] = array($n_old, $orig_type);
           $olds_cursor++;
         } else if ($diff_type == '+') {
           $news[] = array($n_new, $orig_type);
           $news_cursor++;
         }
         if (($diff_type == '-' || $diff_type == ' ') && $orig_type != '-') {
           $n_old++;
         }
         if (($diff_type == '+' || $diff_type == ' ') && $orig_type != '-') {
           $n_new++;
         }
       }
     }
 
     $offsets_old = $this->computeOffsets($old_hunks);
     $offsets_new = $this->computeOffsets($new_hunks);
 
     // Highlight lines that were added on each side or removed on the other
     // side.
     $highlight_old = array();
     $highlight_new = array();
     $last = max(last_key($olds), last_key($news));
     for ($i = 0; $i <= $last; $i++) {
       if (isset($olds[$i])) {
         list($n, $type) = $olds[$i];
         if ($type == '+' ||
             ($type == ' ' && isset($news[$i]) && $news[$i][1] != ' ')) {
           $highlight_old[] = $offsets_old[$n];
         }
       }
       if (isset($news[$i])) {
         list($n, $type) = $news[$i];
         if ($type == '+' ||
             ($type == ' ' && isset($olds[$i]) && $olds[$i][1] != ' ')) {
           $highlight_new[] = $offsets_new[$n];
         }
       }
     }
 
     return array($highlight_old, $highlight_new);
   }
 
   public function makeContextDiff(
     array $hunks,
     $is_new,
     $line_number,
     $line_length,
     $add_context) {
 
     assert_instances_of($hunks, 'DifferentialHunk');
 
     $context = array();
 
     if ($is_new) {
       $prefix = '+';
     } else {
       $prefix = '-';
     }
 
     foreach ($hunks as $hunk) {
       if ($is_new) {
         $offset = $hunk->getNewOffset();
         $length = $hunk->getNewLen();
       } else {
         $offset = $hunk->getOldOffset();
         $length = $hunk->getOldLen();
       }
       $start = $line_number - $offset;
       $end = $start + $line_length;
       // We need to go in if $start == $length, because the last line
       // might be a "\No newline at end of file" marker, which we want
       // to show if the additional context is > 0.
       if ($start <= $length && $end >= 0) {
         $start = $start - $add_context;
         $end = $end + $add_context;
         $hunk_content = array();
         $hunk_pos = array( '-' => 0, '+' => 0 );
         $hunk_offset = array( '-' => null, '+' => null );
         $hunk_last = array( '-' => null, '+' => null );
         foreach (explode("\n", $hunk->getChanges()) as $line) {
           $in_common = strncmp($line, ' ', 1) === 0;
           $in_old = strncmp($line, '-', 1) === 0 || $in_common;
           $in_new = strncmp($line, '+', 1) === 0 || $in_common;
           $in_selected = strncmp($line, $prefix, 1) === 0;
           $skip = !$in_selected && !$in_common;
           if ($hunk_pos[$prefix] <= $end) {
             if ($start <= $hunk_pos[$prefix]) {
               if (!$skip || ($hunk_pos[$prefix] != $start &&
                 $hunk_pos[$prefix] != $end)) {
                   if ($in_old) {
                     if ($hunk_offset['-'] === null) {
                       $hunk_offset['-'] = $hunk_pos['-'];
                     }
                     $hunk_last['-'] = $hunk_pos['-'];
                   }
                   if ($in_new) {
                     if ($hunk_offset['+'] === null) {
                       $hunk_offset['+'] = $hunk_pos['+'];
                     }
                     $hunk_last['+'] = $hunk_pos['+'];
                   }
 
                   $hunk_content[] = $line;
                 }
             }
             if ($in_old) { ++$hunk_pos['-']; }
             if ($in_new) { ++$hunk_pos['+']; }
           }
         }
         if ($hunk_offset['-'] !== null || $hunk_offset['+'] !== null) {
           $header = '@@';
           if ($hunk_offset['-'] !== null) {
             $header .= ' -'.($hunk->getOldOffset() + $hunk_offset['-']).
               ','.($hunk_last['-'] - $hunk_offset['-'] + 1);
           }
           if ($hunk_offset['+'] !== null) {
             $header .= ' +'.($hunk->getNewOffset() + $hunk_offset['+']).
               ','.($hunk_last['+'] - $hunk_offset['+'] + 1);
           }
           $header .= ' @@';
           $context[] = $header;
           $context[] = implode("\n", $hunk_content);
         }
       }
     }
     return implode("\n", $context);
   }
 
   private function computeOffsets(array $hunks) {
     assert_instances_of($hunks, 'DifferentialHunk');
 
     $offsets = array();
     $n = 1;
     foreach ($hunks as $hunk) {
       $new_length = $hunk->getNewLen();
       $new_offset = $hunk->getNewOffset();
 
       for ($i = 0; $i < $new_length; $i++) {
         $offsets[$n] = $new_offset + $i;
         $n++;
       }
     }
 
     return $offsets;
   }
 }
diff --git a/src/applications/differential/render/DifferentialChangesetRenderer.php b/src/applications/differential/render/DifferentialChangesetRenderer.php
index 461ce668b..2aae7bf1c 100644
--- a/src/applications/differential/render/DifferentialChangesetRenderer.php
+++ b/src/applications/differential/render/DifferentialChangesetRenderer.php
@@ -1,667 +1,667 @@
 <?php
 
-abstract class DifferentialChangesetRenderer {
+abstract class DifferentialChangesetRenderer extends Phobject {
 
   private $user;
   private $changeset;
   private $renderingReference;
   private $renderPropertyChangeHeader;
   private $isTopLevel;
   private $isUndershield;
   private $hunkStartLines;
   private $oldLines;
   private $newLines;
   private $oldComments;
   private $newComments;
   private $oldChangesetID;
   private $newChangesetID;
   private $oldAttachesToNewFile;
   private $newAttachesToNewFile;
   private $highlightOld = array();
   private $highlightNew = array();
   private $codeCoverage;
   private $handles;
   private $markupEngine;
   private $oldRender;
   private $newRender;
   private $originalOld;
   private $originalNew;
   private $gaps;
   private $mask;
   private $depths;
   private $originalCharacterEncoding;
   private $showEditAndReplyLinks;
   private $canMarkDone;
   private $objectOwnerPHID;
   private $highlightingDisabled;
 
   private $oldFile = false;
   private $newFile = false;
 
   abstract public function getRendererKey();
 
   public function setShowEditAndReplyLinks($bool) {
     $this->showEditAndReplyLinks = $bool;
     return $this;
   }
 
   public function getShowEditAndReplyLinks() {
     return $this->showEditAndReplyLinks;
   }
 
   public function setHighlightingDisabled($highlighting_disabled) {
     $this->highlightingDisabled = $highlighting_disabled;
     return $this;
   }
 
   public function getHighlightingDisabled() {
     return $this->highlightingDisabled;
   }
 
   public function setOriginalCharacterEncoding($original_character_encoding) {
     $this->originalCharacterEncoding = $original_character_encoding;
     return $this;
   }
 
   public function getOriginalCharacterEncoding() {
     return $this->originalCharacterEncoding;
   }
 
   public function setIsUndershield($is_undershield) {
     $this->isUndershield = $is_undershield;
     return $this;
   }
 
   public function getIsUndershield() {
     return $this->isUndershield;
   }
 
   public function setDepths($depths) {
     $this->depths = $depths;
     return $this;
   }
   protected function getDepths() {
     return $this->depths;
   }
 
   public function setMask($mask) {
     $this->mask = $mask;
     return $this;
   }
   protected function getMask() {
     return $this->mask;
   }
 
   public function setGaps($gaps) {
     $this->gaps = $gaps;
     return $this;
   }
   protected function getGaps() {
     return $this->gaps;
   }
 
   public function attachOldFile(PhabricatorFile $old = null) {
     $this->oldFile = $old;
     return $this;
   }
 
   public function getOldFile() {
     if ($this->oldFile === false) {
       throw new PhabricatorDataNotAttachedException($this);
     }
     return $this->oldFile;
   }
 
   public function hasOldFile() {
     return (bool)$this->oldFile;
   }
 
   public function attachNewFile(PhabricatorFile $new = null) {
     $this->newFile = $new;
     return $this;
   }
 
   public function getNewFile() {
     if ($this->newFile === false) {
       throw new PhabricatorDataNotAttachedException($this);
     }
     return $this->newFile;
   }
 
   public function hasNewFile() {
     return (bool)$this->newFile;
   }
 
   public function setOriginalNew($original_new) {
     $this->originalNew = $original_new;
     return $this;
   }
   protected function getOriginalNew() {
     return $this->originalNew;
   }
 
   public function setOriginalOld($original_old) {
     $this->originalOld = $original_old;
     return $this;
   }
   protected function getOriginalOld() {
     return $this->originalOld;
   }
 
   public function setNewRender($new_render) {
     $this->newRender = $new_render;
     return $this;
   }
   protected function getNewRender() {
     return $this->newRender;
   }
 
   public function setOldRender($old_render) {
     $this->oldRender = $old_render;
     return $this;
   }
   protected function getOldRender() {
     return $this->oldRender;
   }
 
   public function setMarkupEngine(PhabricatorMarkupEngine $markup_engine) {
     $this->markupEngine = $markup_engine;
     return $this;
   }
   public function getMarkupEngine() {
     return $this->markupEngine;
   }
 
   public function setHandles(array $handles) {
     assert_instances_of($handles, 'PhabricatorObjectHandle');
     $this->handles = $handles;
     return $this;
   }
   protected function getHandles() {
     return $this->handles;
   }
 
   public function setCodeCoverage($code_coverage) {
     $this->codeCoverage = $code_coverage;
     return $this;
   }
   protected function getCodeCoverage() {
     return $this->codeCoverage;
   }
 
   public function setHighlightNew($highlight_new) {
     $this->highlightNew = $highlight_new;
     return $this;
   }
   protected function getHighlightNew() {
     return $this->highlightNew;
   }
 
   public function setHighlightOld($highlight_old) {
     $this->highlightOld = $highlight_old;
     return $this;
   }
   protected function getHighlightOld() {
     return $this->highlightOld;
   }
 
   public function setNewAttachesToNewFile($attaches) {
     $this->newAttachesToNewFile = $attaches;
     return $this;
   }
   protected function getNewAttachesToNewFile() {
     return $this->newAttachesToNewFile;
   }
 
   public function setOldAttachesToNewFile($attaches) {
     $this->oldAttachesToNewFile = $attaches;
     return $this;
   }
   protected function getOldAttachesToNewFile() {
     return $this->oldAttachesToNewFile;
   }
 
   public function setNewChangesetID($new_changeset_id) {
     $this->newChangesetID = $new_changeset_id;
     return $this;
   }
   protected function getNewChangesetID() {
     return $this->newChangesetID;
   }
 
   public function setOldChangesetID($old_changeset_id) {
     $this->oldChangesetID = $old_changeset_id;
     return $this;
   }
   protected function getOldChangesetID() {
     return $this->oldChangesetID;
   }
 
   public function setNewComments(array $new_comments) {
     foreach ($new_comments as $line_number => $comments) {
       assert_instances_of($comments, 'PhabricatorInlineCommentInterface');
     }
     $this->newComments = $new_comments;
     return $this;
   }
   protected function getNewComments() {
     return $this->newComments;
   }
 
   public function setOldComments(array $old_comments) {
     foreach ($old_comments as $line_number => $comments) {
       assert_instances_of($comments, 'PhabricatorInlineCommentInterface');
     }
     $this->oldComments = $old_comments;
     return $this;
   }
   protected function getOldComments() {
     return $this->oldComments;
   }
 
   public function setNewLines(array $new_lines) {
     $this->newLines = $new_lines;
     return $this;
   }
   protected function getNewLines() {
     return $this->newLines;
   }
 
   public function setOldLines(array $old_lines) {
     $this->oldLines = $old_lines;
     return $this;
   }
   protected function getOldLines() {
     return $this->oldLines;
   }
 
   public function setHunkStartLines(array $hunk_start_lines) {
     $this->hunkStartLines = $hunk_start_lines;
     return $this;
   }
 
   protected function getHunkStartLines() {
     return $this->hunkStartLines;
   }
 
   public function setUser(PhabricatorUser $user) {
     $this->user = $user;
     return $this;
   }
   protected function getUser() {
     return $this->user;
   }
 
   public function setChangeset(DifferentialChangeset $changeset) {
     $this->changeset = $changeset;
     return $this;
   }
   protected function getChangeset() {
     return $this->changeset;
   }
 
   public function setRenderingReference($rendering_reference) {
     $this->renderingReference = $rendering_reference;
     return $this;
   }
   protected function getRenderingReference() {
     return $this->renderingReference;
   }
 
   public function setRenderPropertyChangeHeader($should_render) {
     $this->renderPropertyChangeHeader = $should_render;
     return $this;
   }
 
   private function shouldRenderPropertyChangeHeader() {
     return $this->renderPropertyChangeHeader;
   }
 
   public function setIsTopLevel($is) {
     $this->isTopLevel = $is;
     return $this;
   }
 
   private function getIsTopLevel() {
     return $this->isTopLevel;
   }
 
   public function setCanMarkDone($can_mark_done) {
     $this->canMarkDone = $can_mark_done;
     return $this;
   }
 
   public function getCanMarkDone() {
     return $this->canMarkDone;
   }
 
   public function setObjectOwnerPHID($phid) {
     $this->objectOwnerPHID = $phid;
     return $this;
   }
 
   public function getObjectOwnerPHID() {
     return $this->objectOwnerPHID;
   }
 
   final public function renderChangesetTable($content) {
     $props = null;
     if ($this->shouldRenderPropertyChangeHeader()) {
       $props = $this->renderPropertyChangeHeader();
     }
 
     $notice = null;
     if ($this->getIsTopLevel()) {
       $force = (!$content && !$props);
       $notice = $this->renderChangeTypeHeader($force);
     }
 
     $undershield = null;
     if ($this->getIsUndershield()) {
       $undershield = $this->renderUndershieldHeader();
     }
 
     $result = $notice.$props.$undershield.$content;
 
     // TODO: Let the user customize their tab width / display style.
     // TODO: We should possibly post-process "\r" as well.
     // TODO: Both these steps should happen earlier.
     $result = str_replace("\t", '  ', $result);
 
     return phutil_safe_html($result);
   }
 
   abstract public function isOneUpRenderer();
   abstract public function renderTextChange(
     $range_start,
     $range_len,
     $rows);
   abstract public function renderFileChange(
     $old = null,
     $new = null,
     $id = 0,
     $vs = 0);
 
   abstract protected function renderChangeTypeHeader($force);
   abstract protected function renderUndershieldHeader();
 
   protected function didRenderChangesetTableContents($contents) {
     return $contents;
   }
 
   /**
    * Render a "shield" over the diff, with a message like "This file is
    * generated and does not need to be reviewed." or "This file was completely
    * deleted." This UI element hides unimportant text so the reviewer doesn't
    * need to scroll past it.
    *
    * The shield includes a link to view the underlying content. This link
    * may force certain rendering modes when the link is clicked:
    *
    *    - `"default"`: Render the diff normally, as though it was not
    *      shielded. This is the default and appropriate if the underlying
    *      diff is a normal change, but was hidden for reasons of not being
    *      important (e.g., generated code).
    *    - `"text"`: Force the text to be shown. This is probably only relevant
    *      when a file is not changed.
    *    - `"whitespace"`: Force the text to be shown, and the diff to be
    *      rendered with all whitespace shown. This is probably only relevant
    *      when a file is changed only by altering whitespace.
    *    - `"none"`: Don't show the link (e.g., text not available).
    *
    * @param   string        Message explaining why the diff is hidden.
    * @param   string|null   Force mode, see above.
    * @return  string        Shield markup.
    */
   abstract public function renderShield($message, $force = 'default');
 
   abstract protected function renderPropertyChangeHeader();
 
   protected function buildPrimitives($range_start, $range_len) {
     $primitives = array();
 
     $hunk_starts = $this->getHunkStartLines();
 
     $mask = $this->getMask();
     $gaps = $this->getGaps();
 
     $old = $this->getOldLines();
     $new = $this->getNewLines();
     $old_render = $this->getOldRender();
     $new_render = $this->getNewRender();
     $old_comments = $this->getOldComments();
     $new_comments = $this->getNewComments();
 
     $size = count($old);
     for ($ii = $range_start; $ii < $range_start + $range_len; $ii++) {
       if (empty($mask[$ii])) {
         list($top, $len) = array_pop($gaps);
         $primitives[] = array(
           'type' => 'context',
           'top' => $top,
           'len' => $len,
         );
 
         $ii += ($len - 1);
         continue;
       }
 
       $ospec = array(
         'type' => 'old',
         'htype' => null,
         'cursor' => $ii,
         'line' => null,
         'oline' => null,
         'render' => null,
       );
 
       $nspec = array(
         'type' => 'new',
         'htype' => null,
         'cursor' => $ii,
         'line' => null,
         'oline' => null,
         'render' => null,
         'copy' => null,
         'coverage' => null,
       );
 
       if (isset($old[$ii])) {
         $ospec['line'] = (int)$old[$ii]['line'];
         $nspec['oline'] = (int)$old[$ii]['line'];
         $ospec['htype'] = $old[$ii]['type'];
         if (isset($old_render[$ii])) {
           $ospec['render'] = $old_render[$ii];
         }
       }
 
       if (isset($new[$ii])) {
         $nspec['line'] = (int)$new[$ii]['line'];
         $ospec['oline'] = (int)$new[$ii]['line'];
         $nspec['htype'] = $new[$ii]['type'];
         if (isset($new_render[$ii])) {
           $nspec['render'] = $new_render[$ii];
         }
       }
 
       if (isset($hunk_starts[$ospec['line']])) {
         $primitives[] = array(
           'type' => 'no-context',
         );
       }
 
       $primitives[] = $ospec;
       $primitives[] = $nspec;
 
       if ($ospec['line'] !== null && isset($old_comments[$ospec['line']])) {
         foreach ($old_comments[$ospec['line']] as $comment) {
           $primitives[] = array(
             'type' => 'inline',
             'comment' => $comment,
             'right' => false,
           );
         }
       }
 
       if ($nspec['line'] !== null && isset($new_comments[$nspec['line']])) {
         foreach ($new_comments[$nspec['line']] as $comment) {
           $primitives[] = array(
             'type' => 'inline',
             'comment' => $comment,
             'right' => true,
           );
         }
       }
 
       if ($hunk_starts && ($ii == $size - 1)) {
         $primitives[] = array(
           'type' => 'no-context',
         );
       }
     }
 
     if ($this->isOneUpRenderer()) {
       $primitives = $this->processPrimitivesForOneUp($primitives);
     }
 
     return $primitives;
   }
 
   private function processPrimitivesForOneUp(array $primitives) {
     // Primitives come out of buildPrimitives() in two-up format, because it
     // is the most general, flexible format. To put them into one-up format,
     // we need to filter and reorder them. In particular:
     //
     //   - We discard unchanged lines in the old file; in one-up format, we
     //     render them only once.
     //   - We group contiguous blocks of old-modified and new-modified lines, so
     //     they render in "block of old, block of new" order instead of
     //     alternating old and new lines.
 
     $out = array();
 
     $old_buf = array();
     $new_buf = array();
     foreach ($primitives as $primitive) {
       $type = $primitive['type'];
 
       if ($type == 'old') {
         if (!$primitive['htype']) {
           // This is a line which appears in both the old file and the new
           // file, or the spacer corresponding to a line added in the new file.
           // Ignore it when rendering a one-up diff.
           continue;
         }
         $old_buf[] = $primitive;
       } else if ($type == 'new') {
         if ($primitive['line'] === null) {
           // This is an empty spacer corresponding to a line removed from the
           // old file. Ignore it when rendering a one-up diff.
           continue;
         }
         if (!$primitive['htype']) {
           // If this line is the same in both versions of the file, put it in
           // the old line buffer. This makes sure inlines on old, unchanged
           // lines end up in the right place.
 
           // First, we need to flush the line buffers if they're not empty.
           if ($old_buf) {
             $out[] = $old_buf;
             $old_buf = array();
           }
           if ($new_buf) {
             $out[] = $new_buf;
             $new_buf = array();
           }
           $old_buf[] = $primitive;
         } else {
           $new_buf[] = $primitive;
         }
       } else if ($type == 'context' || $type == 'no-context') {
         $out[] = $old_buf;
         $out[] = $new_buf;
         $old_buf = array();
         $new_buf = array();
         $out[] = array($primitive);
       } else if ($type == 'inline') {
 
         // If this inline is on the left side, put it after the old lines.
         if (!$primitive['right']) {
           $out[] = $old_buf;
           $out[] = array($primitive);
           $old_buf = array();
         } else {
           $out[] = $old_buf;
           $out[] = $new_buf;
           $out[] = array($primitive);
           $old_buf = array();
           $new_buf = array();
         }
 
       } else {
         throw new Exception(pht("Unknown primitive type '%s'!", $primitive));
       }
     }
 
     $out[] = $old_buf;
     $out[] = $new_buf;
     $out = array_mergev($out);
 
     return $out;
   }
 
   protected function getChangesetProperties($changeset) {
     $old = $changeset->getOldProperties();
     $new = $changeset->getNewProperties();
 
     // When adding files, don't show the uninteresting 644 filemode change.
     if ($changeset->getChangeType() == DifferentialChangeType::TYPE_ADD &&
         $new == array('unix:filemode' => '100644')) {
       unset($new['unix:filemode']);
     }
 
     // Likewise when removing files.
     if ($changeset->getChangeType() == DifferentialChangeType::TYPE_DELETE &&
         $old == array('unix:filemode' => '100644')) {
       unset($old['unix:filemode']);
     }
 
     if ($this->hasOldFile()) {
       $file = $this->getOldFile();
       if ($file->getImageWidth()) {
         $dimensions = $file->getImageWidth().'x'.$file->getImageHeight();
         $old['file:dimensions'] = $dimensions;
       }
       $old['file:mimetype'] = $file->getMimeType();
       $old['file:size'] = phutil_format_bytes($file->getByteSize());
     }
 
     if ($this->hasNewFile()) {
       $file = $this->getNewFile();
       if ($file->getImageWidth()) {
         $dimensions = $file->getImageWidth().'x'.$file->getImageHeight();
         $new['file:dimensions'] = $dimensions;
       }
       $new['file:mimetype'] = $file->getMimeType();
       $new['file:size'] = phutil_format_bytes($file->getByteSize());
     }
 
     return array($old, $new);
   }
 
   public function renderUndoTemplates() {
     $views = array(
       'l' => id(new PHUIDiffInlineCommentUndoView())->setIsOnRight(false),
       'r' => id(new PHUIDiffInlineCommentUndoView())->setIsOnRight(true),
     );
 
     foreach ($views as $key => $view) {
       $scaffold = $this->getRowScaffoldForInline($view);
       $views[$key] = id(new PHUIDiffInlineCommentTableScaffold())
         ->addRowScaffold($scaffold);
     }
 
     return $views;
   }
 
 }
diff --git a/src/applications/differential/render/DifferentialRawDiffRenderer.php b/src/applications/differential/render/DifferentialRawDiffRenderer.php
index 65303e55d..0b826e9ba 100644
--- a/src/applications/differential/render/DifferentialRawDiffRenderer.php
+++ b/src/applications/differential/render/DifferentialRawDiffRenderer.php
@@ -1,65 +1,66 @@
 <?php
-final class DifferentialRawDiffRenderer {
+
+final class DifferentialRawDiffRenderer extends Phobject {
 
   private $changesets;
   private $format = 'unified';
   private $viewer;
 
   public function setFormat($format) {
     $this->format = $format;
     return $this;
   }
 
   public function getFormat() {
     return $this->format;
   }
 
   public function setChangesets(array $changesets) {
     assert_instances_of($changesets, 'DifferentialChangeset');
 
     $this->changesets = $changesets;
     return $this;
   }
 
   public function getChangesets() {
     return $this->changesets;
   }
 
   public function setViewer(PhabricatorUser $viewer) {
     $this->viewer = $viewer;
     return $this;
   }
 
   public function getViewer() {
     return $this->viewer;
   }
 
   public function buildPatch() {
     $diff = new DifferentialDiff();
     $diff->attachChangesets($this->getChangesets());
 
     $raw_changes = $diff->buildChangesList();
     $changes = array();
     foreach ($raw_changes as $changedict) {
       $changes[] = ArcanistDiffChange::newFromDictionary($changedict);
     }
 
     $viewer = $this->getViewer();
     $loader = id(new PhabricatorFileBundleLoader())
       ->setViewer($viewer);
 
     $bundle = ArcanistBundle::newFromChanges($changes);
     $bundle->setLoadFileDataCallback(array($loader, 'loadFileData'));
 
     $format = $this->getFormat();
     switch ($format) {
       case 'git':
         return $bundle->toGitPatch();
         break;
       case 'unified':
       default:
         return $bundle->toUnifiedDiff();
         break;
     }
   }
 }
diff --git a/src/applications/differential/storage/DifferentialInlineComment.php b/src/applications/differential/storage/DifferentialInlineComment.php
index 4af5ea568..7fe929942 100644
--- a/src/applications/differential/storage/DifferentialInlineComment.php
+++ b/src/applications/differential/storage/DifferentialInlineComment.php
@@ -1,284 +1,285 @@
 <?php
 
 final class DifferentialInlineComment
+  extends Phobject
   implements PhabricatorInlineCommentInterface {
 
   private $proxy;
   private $syntheticAuthor;
   private $isGhost;
 
   public function __construct() {
     $this->proxy = new DifferentialTransactionComment();
   }
 
   public function __clone() {
     $this->proxy = clone $this->proxy;
   }
 
   public function getTransactionCommentForSave() {
     $content_source = PhabricatorContentSource::newForSource(
       PhabricatorContentSource::SOURCE_LEGACY,
       array());
 
     $this->proxy
       ->setViewPolicy('public')
       ->setEditPolicy($this->getAuthorPHID())
       ->setContentSource($content_source)
       ->attachIsHidden(false)
       ->setCommentVersion(1);
 
     return $this->proxy;
   }
 
   public function openTransaction() {
     $this->proxy->openTransaction();
   }
 
   public function saveTransaction() {
     $this->proxy->saveTransaction();
   }
 
   public function save() {
     $this->getTransactionCommentForSave()->save();
 
     return $this;
   }
 
   public function delete() {
     $this->proxy->delete();
 
     return $this;
   }
 
   public function supportsHiding() {
     if ($this->getSyntheticAuthor()) {
       return false;
     }
     return true;
   }
 
   public function isHidden() {
     if (!$this->supportsHiding()) {
       return false;
     }
     return $this->proxy->getIsHidden();
   }
 
   public function getID() {
     return $this->proxy->getID();
   }
 
   public function getPHID() {
     return $this->proxy->getPHID();
   }
 
   public static function newFromModernComment(
     DifferentialTransactionComment $comment) {
 
     $obj = new DifferentialInlineComment();
     $obj->proxy = $comment;
 
     return $obj;
   }
 
   public function setSyntheticAuthor($synthetic_author) {
     $this->syntheticAuthor = $synthetic_author;
     return $this;
   }
 
   public function getSyntheticAuthor() {
     return $this->syntheticAuthor;
   }
 
   public function isCompatible(PhabricatorInlineCommentInterface $comment) {
     return
       ($this->getAuthorPHID() === $comment->getAuthorPHID()) &&
       ($this->getSyntheticAuthor() === $comment->getSyntheticAuthor()) &&
       ($this->getContent() === $comment->getContent());
   }
 
   public function setContent($content) {
     $this->proxy->setContent($content);
     return $this;
   }
 
   public function getContent() {
     return $this->proxy->getContent();
   }
 
   public function isDraft() {
     return !$this->proxy->getTransactionPHID();
   }
 
   public function setChangesetID($id) {
     $this->proxy->setChangesetID($id);
     return $this;
   }
 
   public function getChangesetID() {
     return $this->proxy->getChangesetID();
   }
 
   public function setIsNewFile($is_new) {
     $this->proxy->setIsNewFile($is_new);
     return $this;
   }
 
   public function getIsNewFile() {
     return $this->proxy->getIsNewFile();
   }
 
   public function setLineNumber($number) {
     $this->proxy->setLineNumber($number);
     return $this;
   }
 
   public function getLineNumber() {
     return $this->proxy->getLineNumber();
   }
 
   public function setLineLength($length) {
     $this->proxy->setLineLength($length);
     return $this;
   }
 
   public function getLineLength() {
     return $this->proxy->getLineLength();
   }
 
   public function setCache($cache) {
     return $this;
   }
 
   public function getCache() {
     return null;
   }
 
   public function setAuthorPHID($phid) {
     $this->proxy->setAuthorPHID($phid);
     return $this;
   }
 
   public function getAuthorPHID() {
     return $this->proxy->getAuthorPHID();
   }
 
   public function setRevision(DifferentialRevision $revision) {
     $this->proxy->setRevisionPHID($revision->getPHID());
     return $this;
   }
 
   public function getRevisionPHID() {
     return $this->proxy->getRevisionPHID();
   }
 
   // Although these are purely transitional, they're also *extra* dumb.
 
   public function setRevisionID($revision_id) {
     $revision = id(new DifferentialRevision())->load($revision_id);
     return $this->setRevision($revision);
   }
 
   public function getRevisionID() {
     $phid = $this->proxy->getRevisionPHID();
     if (!$phid) {
       return null;
     }
 
     $revision = id(new DifferentialRevision())->loadOneWhere(
       'phid = %s',
       $phid);
     if (!$revision) {
       return null;
     }
     return $revision->getID();
   }
 
   // When setting a comment ID, we also generate a phantom transaction PHID for
   // the future transaction.
 
   public function setCommentID($id) {
     $this->proxy->setTransactionPHID(
       PhabricatorPHID::generateNewPHID(
         PhabricatorApplicationTransactionTransactionPHIDType::TYPECONST,
         DifferentialRevisionPHIDType::TYPECONST));
     return $this;
   }
 
   public function setReplyToCommentPHID($phid) {
     $this->proxy->setReplyToCommentPHID($phid);
     return $this;
   }
 
   public function getReplyToCommentPHID() {
     return $this->proxy->getReplyToCommentPHID();
   }
 
   public function setHasReplies($has_replies) {
     $this->proxy->setHasReplies($has_replies);
     return $this;
   }
 
   public function getHasReplies() {
     return $this->proxy->getHasReplies();
   }
 
   public function setIsDeleted($is_deleted) {
     $this->proxy->setIsDeleted($is_deleted);
     return $this;
   }
 
   public function getIsDeleted() {
     return $this->proxy->getIsDeleted();
   }
 
   public function setFixedState($state) {
     $this->proxy->setFixedState($state);
     return $this;
   }
 
   public function getFixedState() {
     return $this->proxy->getFixedState();
   }
 
   public function setIsGhost($is_ghost) {
     $this->isGhost = $is_ghost;
     return $this;
   }
 
   public function getIsGhost() {
     return $this->isGhost;
   }
 
   public function makeEphemeral() {
     $this->proxy->makeEphemeral();
     return $this;
   }
 
 
 /* -(  PhabricatorMarkupInterface Implementation  )-------------------------- */
 
 
   public function getMarkupFieldKey($field) {
     // We can't use ID because synthetic comments don't have it.
     return 'DI:'.PhabricatorHash::digest($this->getContent());
   }
 
   public function newMarkupEngine($field) {
     return PhabricatorMarkupEngine::newDifferentialMarkupEngine();
   }
 
   public function getMarkupText($field) {
     return $this->getContent();
   }
 
   public function didMarkupText($field, $output, PhutilMarkupEngine $engine) {
     return $output;
   }
 
   public function shouldUseMarkupCache($field) {
     // Only cache submitted comments.
     return ($this->getID() && !$this->isDraft());
   }
 
 }
diff --git a/src/applications/differential/storage/DifferentialReviewer.php b/src/applications/differential/storage/DifferentialReviewer.php
index e3b1e0ac2..1ad7fbc8b 100644
--- a/src/applications/differential/storage/DifferentialReviewer.php
+++ b/src/applications/differential/storage/DifferentialReviewer.php
@@ -1,56 +1,56 @@
 <?php
 
-final class DifferentialReviewer {
+final class DifferentialReviewer extends Phobject {
 
   private $reviewerPHID;
   private $status;
   private $diffID;
   private $authority = array();
 
   public function __construct($reviewer_phid, array $edge_data) {
     $this->reviewerPHID = $reviewer_phid;
     $this->status = idx($edge_data, 'status');
     $this->diffID = idx($edge_data, 'diff');
   }
 
   public function getReviewerPHID() {
     return $this->reviewerPHID;
   }
 
   public function getStatus() {
     return $this->status;
   }
 
   public function getDiffID() {
     return $this->diffID;
   }
 
   public function isUser() {
     $user_type = PhabricatorPeopleUserPHIDType::TYPECONST;
     return (phid_get_type($this->getReviewerPHID()) == $user_type);
   }
 
   public function attachAuthority(PhabricatorUser $user, $has_authority) {
     $this->authority[$user->getPHID()] = $has_authority;
     return $this;
   }
 
   public function hasAuthority(PhabricatorUser $viewer) {
     // It would be nice to use assertAttachedKey() here, but we don't extend
     // PhabricatorLiskDAO, and faking that seems sketchy.
 
     $viewer_phid = $viewer->getPHID();
     if (!array_key_exists($viewer_phid, $this->authority)) {
       throw new Exception(pht('You must %s first!', 'attachAuthority()'));
     }
     return $this->authority[$viewer_phid];
   }
 
   public function getEdgeData() {
     return array(
       'status' => $this->status,
       'diffID' => $this->diffID,
     );
   }
 
 }
diff --git a/src/applications/differential/view/DifferentialChangesetFileTreeSideNavBuilder.php b/src/applications/differential/view/DifferentialChangesetFileTreeSideNavBuilder.php
index 031db2be4..c8cb8c237 100644
--- a/src/applications/differential/view/DifferentialChangesetFileTreeSideNavBuilder.php
+++ b/src/applications/differential/view/DifferentialChangesetFileTreeSideNavBuilder.php
@@ -1,137 +1,137 @@
 <?php
 
-final class DifferentialChangesetFileTreeSideNavBuilder {
+final class DifferentialChangesetFileTreeSideNavBuilder extends Phobject {
 
   private $title;
   private $baseURI;
   private $anchorName;
   private $collapsed = false;
 
   public function setAnchorName($anchor_name) {
     $this->anchorName = $anchor_name;
     return $this;
   }
   public function getAnchorName() {
     return $this->anchorName;
   }
 
   public function setBaseURI(PhutilURI $base_uri) {
     $this->baseURI = $base_uri;
     return $this;
   }
   public function getBaseURI() {
     return $this->baseURI;
   }
 
   public function setTitle($title) {
     $this->title = $title;
     return $this;
   }
   public function getTitle() {
     return $this->title;
   }
 
   public function setCollapsed($collapsed) {
     $this->collapsed = $collapsed;
     return $this;
   }
 
   public function build(array $changesets) {
     assert_instances_of($changesets, 'DifferentialChangeset');
 
     $nav = new AphrontSideNavFilterView();
     $nav->setBaseURI($this->getBaseURI());
     $nav->setFlexible(true);
     $nav->setCollapsed($this->collapsed);
 
     $anchor = $this->getAnchorName();
 
     $tree = new PhutilFileTree();
     foreach ($changesets as $changeset) {
       try {
         $tree->addPath($changeset->getFilename(), $changeset);
       } catch (Exception $ex) {
         // TODO: See T1702. When viewing the versus diff of diffs, we may
         // have files with the same filename. For example, if you have a setup
         // like this in SVN:
         //
         //  a/
         //    README
         //  b/
         //    README
         //
         // ...and you run "arc diff" once from a/, and again from b/, you'll
         // get two diffs with path README. However, in the versus diff view we
         // will compute their absolute repository paths and detect that they
         // aren't really the same file. This is correct, but causes us to
         // throw when inserting them.
         //
         // We should probably compute the smallest unique path for each file
         // and show these as "a/README" and "b/README" when diffed against
         // one another. However, we get this wrong in a lot of places (the
         // other TOC shows two "README" files, and we generate the same anchor
         // hash for both) so I'm just stopping the bleeding until we can get
         // a proper fix in place.
       }
     }
 
     require_celerity_resource('phabricator-filetree-view-css');
 
     $filetree = array();
 
     $path = $tree;
     while (($path = $path->getNextNode())) {
       $data = $path->getData();
 
       $name = $path->getName();
       $style = 'padding-left: '.(2 + (3 * $path->getDepth())).'px';
 
       $href = null;
       if ($data) {
         $href = '#'.$data->getAnchorName();
         $title = $name;
         $icon = id(new PHUIIconView())
           ->setIconFont('fa-file-text-o bluetext');
       } else {
         $name .= '/';
         $title = $path->getFullPath().'/';
         $icon = id(new PHUIIconView())
           ->setIconFont('fa-folder-open blue');
       }
 
       $name_element = phutil_tag(
         'span',
         array(
           'class' => 'phabricator-filetree-name',
         ),
         $name);
 
       $filetree[] = javelin_tag(
         $href ? 'a' : 'span',
         array(
           'href' => $href,
           'style' => $style,
           'title' => $title,
           'class' => 'phabricator-filetree-item',
         ),
         array($icon, $name_element));
     }
     $tree->destroy();
 
     $filetree = phutil_tag(
       'div',
       array(
         'class' => 'phabricator-filetree',
       ),
       $filetree);
 
     Javelin::initBehavior('phabricator-file-tree', array());
 
     $nav->addLabel(pht('Changed Files'));
     $nav->addCustomBlock($filetree);
     $nav->setActive(true);
     $nav->selectFilter(null);
     return $nav;
   }
 
 }
diff --git a/src/applications/diffusion/DiffusionLintSaveRunner.php b/src/applications/diffusion/DiffusionLintSaveRunner.php
index 7b652cf5f..b726089fe 100644
--- a/src/applications/diffusion/DiffusionLintSaveRunner.php
+++ b/src/applications/diffusion/DiffusionLintSaveRunner.php
@@ -1,306 +1,306 @@
 <?php
 
-final class DiffusionLintSaveRunner {
+final class DiffusionLintSaveRunner extends Phobject {
   private $arc = 'arc';
   private $severity = ArcanistLintSeverity::SEVERITY_ADVICE;
   private $all = false;
   private $chunkSize = 256;
   private $needsBlame = false;
 
   private $svnRoot;
   private $lintCommit;
   private $branch;
   private $conn;
   private $deletes = array();
   private $inserts = array();
   private $blame = array();
 
 
   public function setArc($path) {
     $this->arc = $path;
     return $this;
   }
 
   public function setSeverity($string) {
     $this->severity = $string;
     return $this;
   }
 
   public function setAll($bool) {
     $this->all = $bool;
     return $this;
   }
 
   public function setChunkSize($number) {
     $this->chunkSize = $number;
     return $this;
   }
 
   public function setNeedsBlame($boolean) {
     $this->needsBlame = $boolean;
     return $this;
   }
 
 
   public function run($dir) {
     $working_copy = ArcanistWorkingCopyIdentity::newFromPath($dir);
     $configuration_manager = new ArcanistConfigurationManager();
     $configuration_manager->setWorkingCopyIdentity($working_copy);
     $api = ArcanistRepositoryAPI::newAPIFromConfigurationManager(
       $configuration_manager);
 
     $this->svnRoot = id(new PhutilURI($api->getSourceControlPath()))->getPath();
     if ($api instanceof ArcanistGitAPI) {
       $svn_fetch = $api->getGitConfig('svn-remote.svn.fetch');
       list($this->svnRoot) = explode(':', $svn_fetch);
       if ($this->svnRoot != '') {
         $this->svnRoot = '/'.$this->svnRoot;
       }
     }
 
     $callsign = $configuration_manager->getConfigFromAnySource(
       'repository.callsign');
     $uuid = $api->getRepositoryUUID();
     $remote_uri = $api->getRemoteURI();
 
     $repository_query = id(new PhabricatorRepositoryQuery())
       ->setViewer(PhabricatorUser::getOmnipotentUser());
 
     if ($callsign) {
       $repository_query->withCallsigns(array($callsign));
     } else if ($uuid) {
       $repository_query->withUUIDs(array($uuid));
     } else if ($remote_uri) {
       $repository_query->withRemoteURIs(array($remote_uri));
     }
 
     $repository = $repository_query->executeOne();
     $branch_name = $api->getBranchName();
 
     if (!$repository) {
       throw new Exception(pht('No repository was found.'));
     }
 
     $this->branch = PhabricatorRepositoryBranch::loadOrCreateBranch(
       $repository->getID(),
       $branch_name);
     $this->conn = $this->branch->establishConnection('w');
 
     $this->lintCommit = null;
     if (!$this->all) {
       $this->lintCommit = $this->branch->getLintCommit();
     }
 
     if ($this->lintCommit) {
       try {
         $commit = $this->lintCommit;
         if ($this->svnRoot) {
           $commit = $api->getCanonicalRevisionName('@'.$commit);
         }
         $all_files = $api->getChangedFiles($commit);
       } catch (ArcanistCapabilityNotSupportedException $ex) {
         $this->lintCommit = null;
       }
     }
 
 
     if (!$this->lintCommit) {
       $where = ($this->svnRoot
         ? qsprintf($this->conn, 'AND path LIKE %>', $this->svnRoot.'/')
         : '');
       queryfx(
         $this->conn,
         'DELETE FROM %T WHERE branchID = %d %Q',
         PhabricatorRepository::TABLE_LINTMESSAGE,
         $this->branch->getID(),
         $where);
       $all_files = $api->getAllFiles();
     }
 
     $count = 0;
 
     $files = array();
     foreach ($all_files as $file => $val) {
       $count++;
       if (!$this->lintCommit) {
         $file = $val;
       } else {
         $this->deletes[] = $this->svnRoot.'/'.$file;
         if ($val & ArcanistRepositoryAPI::FLAG_DELETED) {
           continue;
         }
       }
       $files[$file] = $file;
 
       if (count($files) >= $this->chunkSize) {
         $this->runArcLint($files);
         $files = array();
       }
     }
 
     $this->runArcLint($files);
     $this->saveLintMessages();
 
     $this->lintCommit = $api->getUnderlyingWorkingCopyRevision();
     $this->branch->setLintCommit($this->lintCommit);
     $this->branch->save();
 
     if ($this->blame) {
       $this->blameAuthors();
       $this->blame = array();
     }
 
     return $count;
   }
 
 
   private function runArcLint(array $files) {
     if (!$files) {
       return;
     }
 
     echo '.';
     try {
       $future = new ExecFuture(
         '%C lint --severity %s --output json %Ls',
         $this->arc,
         $this->severity,
         $files);
 
       foreach (new LinesOfALargeExecFuture($future) as $json) {
         $paths = null;
         try {
           $paths = phutil_json_decode($json);
         } catch (PhutilJSONParserException $ex) {
           fprintf(STDERR, pht('Invalid JSON: %s', $json)."\n");
           continue;
         }
 
         foreach ($paths as $path => $messages) {
           if (!isset($files[$path])) {
             continue;
           }
 
           foreach ($messages as $message) {
             $line = idx($message, 'line', 0);
 
             $this->inserts[] = qsprintf(
               $this->conn,
               '(%d, %s, %d, %s, %s, %s, %s)',
               $this->branch->getID(),
               $this->svnRoot.'/'.$path,
               $line,
               idx($message, 'code', ''),
               idx($message, 'severity', ''),
               idx($message, 'name', ''),
               idx($message, 'description', ''));
 
             if ($line && $this->needsBlame) {
               $this->blame[$path][$line] = true;
             }
           }
 
           if (count($this->deletes) >= 1024 || count($this->inserts) >= 256) {
             $this->saveLintMessages();
           }
         }
       }
 
     } catch (Exception $ex) {
       fprintf(STDERR, $ex->getMessage()."\n");
     }
   }
 
 
   private function saveLintMessages() {
     $this->conn->openTransaction();
 
     foreach (array_chunk($this->deletes, 1024) as $paths) {
       queryfx(
         $this->conn,
         'DELETE FROM %T WHERE branchID = %d AND path IN (%Ls)',
         PhabricatorRepository::TABLE_LINTMESSAGE,
         $this->branch->getID(),
         $paths);
     }
 
     foreach (array_chunk($this->inserts, 256) as $values) {
       queryfx(
         $this->conn,
         'INSERT INTO %T
           (branchID, path, line, code, severity, name, description)
           VALUES %Q',
         PhabricatorRepository::TABLE_LINTMESSAGE,
         implode(', ', $values));
     }
 
     $this->conn->saveTransaction();
 
     $this->deletes = array();
     $this->inserts = array();
   }
 
 
   private function blameAuthors() {
     $repository = id(new PhabricatorRepositoryQuery())
       ->setViewer(PhabricatorUser::getOmnipotentUser())
       ->withIDs(array($this->branch->getRepositoryID()))
       ->executeOne();
 
     $queries = array();
     $futures = array();
     foreach ($this->blame as $path => $lines) {
       $drequest = DiffusionRequest::newFromDictionary(array(
         'user' => PhabricatorUser::getOmnipotentUser(),
         'repository' => $repository,
         'branch' => $this->branch->getName(),
         'path' => $path,
         'commit' => $this->lintCommit,
       ));
       $query = DiffusionFileContentQuery::newFromDiffusionRequest($drequest)
         ->setNeedsBlame(true);
       $queries[$path] = $query;
       $futures[$path] = $query->getFileContentFuture();
     }
 
     $authors = array();
 
     $futures = id(new FutureIterator($futures))
       ->limit(8);
     foreach ($futures as $path => $future) {
       $queries[$path]->loadFileContentFromFuture($future);
       list(, $rev_list, $blame_dict) = $queries[$path]->getBlameData();
       foreach (array_keys($this->blame[$path]) as $line) {
         $commit_identifier = $rev_list[$line - 1];
         $author = idx($blame_dict[$commit_identifier], 'authorPHID');
         if ($author) {
           $authors[$author][$path][] = $line;
         }
       }
     }
 
     if ($authors) {
       $this->conn->openTransaction();
 
       foreach ($authors as $author => $paths) {
         $where = array();
         foreach ($paths as $path => $lines) {
           $where[] = qsprintf(
             $this->conn,
             '(path = %s AND line IN (%Ld))',
             $this->svnRoot.'/'.$path,
             $lines);
         }
         queryfx(
           $this->conn,
           'UPDATE %T SET authorPHID = %s WHERE %Q',
           PhabricatorRepository::TABLE_LINTMESSAGE,
           $author,
           implode(' OR ', $where));
       }
 
       $this->conn->saveTransaction();
     }
   }
 
 }
diff --git a/src/applications/diffusion/application/PhabricatorDiffusionApplication.php b/src/applications/diffusion/application/PhabricatorDiffusionApplication.php
index 0f44db3d1..271efc2a5 100644
--- a/src/applications/diffusion/application/PhabricatorDiffusionApplication.php
+++ b/src/applications/diffusion/application/PhabricatorDiffusionApplication.php
@@ -1,173 +1,178 @@
 <?php
 
 final class PhabricatorDiffusionApplication extends PhabricatorApplication {
 
   public function getName() {
     return pht('Diffusion');
   }
 
   public function getShortDescription() {
     return pht('Host and Browse Repositories');
   }
 
   public function getBaseURI() {
     return '/diffusion/';
   }
 
   public function getFontIcon() {
     return 'fa-code';
   }
 
   public function isPinnedByDefault(PhabricatorUser $viewer) {
     return true;
   }
 
   public function getHelpDocumentationArticles(PhabricatorUser $viewer) {
     return array(
       array(
         'name' => pht('Diffusion User Guide'),
         'href' => PhabricatorEnv::getDoclink('Diffusion User Guide'),
       ),
     );
   }
 
   public function getFactObjectsForAnalysis() {
     return array(
       new PhabricatorRepositoryCommit(),
     );
   }
 
   public function getEventListeners() {
     return array(
       new DiffusionHovercardEventListener(),
     );
   }
 
   public function getRemarkupRules() {
     return array(
       new DiffusionCommitRemarkupRule(),
       new DiffusionRepositoryRemarkupRule(),
       new DiffusionRepositoryByIDRemarkupRule(),
     );
   }
 
   public function getRoutes() {
     return array(
       '/r(?P<callsign>[A-Z]+)(?P<commit>[a-z0-9]+)'
         => 'DiffusionCommitController',
       '/diffusion/' => array(
         '(?:query/(?P<queryKey>[^/]+)/)?'
           => 'DiffusionRepositoryListController',
         'new/' => 'DiffusionRepositoryNewController',
         '(?P<edit>create)/' => 'DiffusionRepositoryCreateController',
         '(?P<edit>import)/' => 'DiffusionRepositoryCreateController',
         'pushlog/' => array(
           '(?:query/(?P<queryKey>[^/]+)/)?' => 'DiffusionPushLogListController',
           'view/(?P<id>\d+)/' => 'DiffusionPushEventViewController',
         ),
         '(?P<callsign>[A-Z]+)/' => array(
           '' => 'DiffusionRepositoryController',
 
           'repository/(?P<dblob>.*)'    => 'DiffusionRepositoryController',
           'change/(?P<dblob>.*)'        => 'DiffusionChangeController',
           'history/(?P<dblob>.*)'       => 'DiffusionHistoryController',
           'browse/(?P<dblob>.*)'        => 'DiffusionBrowseMainController',
           'lastmodified/(?P<dblob>.*)'  => 'DiffusionLastModifiedController',
           'diff/'                       => 'DiffusionDiffController',
           'tags/(?P<dblob>.*)'          => 'DiffusionTagListController',
           'branches/(?P<dblob>.*)'      => 'DiffusionBranchTableController',
           'refs/(?P<dblob>.*)'          => 'DiffusionRefTableController',
           'lint/(?P<dblob>.*)'          => 'DiffusionLintController',
           'commit/(?P<commit>[a-z0-9]+)/branches/'
             => 'DiffusionCommitBranchesController',
           'commit/(?P<commit>[a-z0-9]+)/tags/'
             => 'DiffusionCommitTagsController',
           'commit/(?P<commit>[a-z0-9]+)/edit/'
             => 'DiffusionCommitEditController',
           'edit/' => array(
             '' => 'DiffusionRepositoryEditMainController',
             'basic/' => 'DiffusionRepositoryEditBasicController',
             'encoding/' => 'DiffusionRepositoryEditEncodingController',
             'activate/' => 'DiffusionRepositoryEditActivateController',
             'dangerous/' => 'DiffusionRepositoryEditDangerousController',
             'branches/' => 'DiffusionRepositoryEditBranchesController',
             'subversion/' => 'DiffusionRepositoryEditSubversionController',
             'actions/' => 'DiffusionRepositoryEditActionsController',
             '(?P<edit>remote)/' => 'DiffusionRepositoryCreateController',
             '(?P<edit>policy)/' => 'DiffusionRepositoryCreateController',
             'storage/' => 'DiffusionRepositoryEditStorageController',
             'delete/' => 'DiffusionRepositoryEditDeleteController',
             'hosting/' => 'DiffusionRepositoryEditHostingController',
             '(?P<serve>serve)/' => 'DiffusionRepositoryEditHostingController',
             'update/' => 'DiffusionRepositoryEditUpdateController',
             'symbol/' => 'DiffusionRepositorySymbolsController',
             'staging/' => 'DiffusionRepositoryEditStagingController',
           ),
           'pathtree/(?P<dblob>.*)' => 'DiffusionPathTreeController',
           'mirror/' => array(
             'edit/(?:(?P<id>\d+)/)?' => 'DiffusionMirrorEditController',
             'delete/(?P<id>\d+)/' => 'DiffusionMirrorDeleteController',
           ),
         ),
 
         // NOTE: This must come after the rule above; it just gives us a
         // catch-all for serving repositories over HTTP. We must accept
         // requests without the trailing "/" because SVN commands don't
         // necessarily include it.
         '(?P<callsign>[A-Z]+)(/|$).*' => 'DiffusionRepositoryDefaultController',
 
         'inline/' => array(
           'edit/(?P<phid>[^/]+)/' => 'DiffusionInlineCommentController',
           'preview/(?P<phid>[^/]+)/'
             => 'DiffusionInlineCommentPreviewController',
         ),
         'services/' => array(
           'path/' => array(
             'complete/' => 'DiffusionPathCompleteController',
             'validate/' => 'DiffusionPathValidateController',
           ),
         ),
         'symbol/(?P<name>[^/]+)/' => 'DiffusionSymbolController',
         'external/' => 'DiffusionExternalController',
         'lint/' => 'DiffusionLintController',
       ),
     );
   }
 
   public function getApplicationOrder() {
     return 0.120;
   }
 
   protected function getCustomCapabilities() {
     return array(
-      DiffusionDefaultViewCapability::CAPABILITY => array(),
+      DiffusionDefaultViewCapability::CAPABILITY => array(
+        'template' => PhabricatorRepositoryRepositoryPHIDType::TYPECONST,
+      ),
       DiffusionDefaultEditCapability::CAPABILITY => array(
         'default' => PhabricatorPolicies::POLICY_ADMIN,
+        'template' => PhabricatorRepositoryRepositoryPHIDType::TYPECONST,
+      ),
+      DiffusionDefaultPushCapability::CAPABILITY => array(
+        'template' => PhabricatorRepositoryRepositoryPHIDType::TYPECONST,
       ),
-      DiffusionDefaultPushCapability::CAPABILITY => array(),
       DiffusionCreateRepositoriesCapability::CAPABILITY => array(
         'default' => PhabricatorPolicies::POLICY_ADMIN,
       ),
     );
   }
 
   public function getMailCommandObjects() {
     return array(
       'commit' => array(
         'name' => pht('Email Commands: Commits'),
         'header' => pht('Interacting with Commits'),
         'object' => new PhabricatorRepositoryCommit(),
         'summary' => pht(
           'This page documents the commands you can use to interact with '.
           'commits and audits in Diffusion.'),
       ),
     );
   }
 
   public function getApplicationSearchDocumentTypes() {
     return array(
       PhabricatorRepositoryCommitPHIDType::TYPECONST,
     );
   }
 
 }
diff --git a/src/applications/diffusion/controller/DiffusionRepositoryCreateController.php b/src/applications/diffusion/controller/DiffusionRepositoryCreateController.php
index 269523915..9b6f4eee6 100644
--- a/src/applications/diffusion/controller/DiffusionRepositoryCreateController.php
+++ b/src/applications/diffusion/controller/DiffusionRepositoryCreateController.php
@@ -1,903 +1,903 @@
 <?php
 
 final class DiffusionRepositoryCreateController
   extends DiffusionRepositoryEditController {
 
   private $edit;
   private $repository;
 
   protected function processDiffusionRequest(AphrontRequest $request) {
     $viewer = $request->getUser();
     $this->edit = $request->getURIData('edit');
 
     // NOTE: We can end up here via either "Create Repository", or via
     // "Import Repository", or via "Edit Remote", or via "Edit Policies". In
     // the latter two cases, we show only a few of the pages.
 
     $repository = null;
     $service = null;
     switch ($this->edit) {
       case 'remote':
       case 'policy':
         $repository = $this->getDiffusionRequest()->getRepository();
 
         // Make sure we have CAN_EDIT.
         PhabricatorPolicyFilter::requireCapability(
           $viewer,
           $repository,
           PhabricatorPolicyCapability::CAN_EDIT);
 
         $this->setRepository($repository);
 
         $cancel_uri = $this->getRepositoryControllerURI($repository, 'edit/');
         break;
       case 'import':
       case 'create':
         $this->requireApplicationCapability(
           DiffusionCreateRepositoriesCapability::CAPABILITY);
 
         // Pick a random open service to allocate this repository on, if any
         // exist. If there are no services, we aren't in cluster mode and
         // will allocate locally. If there are services but none permit
         // allocations, we fail.
         $services = id(new AlmanacServiceQuery())
           ->setViewer(PhabricatorUser::getOmnipotentUser())
           ->withServiceClasses(
             array(
               'AlmanacClusterRepositoryServiceType',
             ))
           ->execute();
         if ($services) {
           // Filter out services which do not permit new allocations.
           foreach ($services as $key => $possible_service) {
             if ($possible_service->getAlmanacPropertyValue('closed')) {
               unset($services[$key]);
             }
           }
 
           if (!$services) {
             throw new Exception(
               pht(
                 'This install is configured in cluster mode, but all '.
                 'available repository cluster services are closed to new '.
                 'allocations. At least one service must be open to allow '.
                 'new allocations to take place.'));
           }
 
           shuffle($services);
           $service = head($services);
         }
 
         $cancel_uri = $this->getApplicationURI('new/');
         break;
       default:
         throw new Exception(pht('Invalid edit operation!'));
     }
 
     $form = id(new PHUIPagedFormView())
       ->setUser($viewer)
       ->setCancelURI($cancel_uri);
 
     switch ($this->edit) {
       case 'remote':
         $title = pht('Edit Remote');
         $form
           ->addPage('remote-uri', $this->buildRemoteURIPage())
           ->addPage('auth', $this->buildAuthPage());
         break;
       case 'policy':
         $title = pht('Edit Policies');
         $form
           ->addPage('policy', $this->buildPolicyPage());
         break;
       case 'create':
         $title = pht('Create Repository');
         $form
           ->addPage('vcs', $this->buildVCSPage())
           ->addPage('name', $this->buildNamePage())
           ->addPage('policy', $this->buildPolicyPage())
           ->addPage('done', $this->buildDonePage());
         break;
       case 'import':
         $title = pht('Import Repository');
         $form
           ->addPage('vcs', $this->buildVCSPage())
           ->addPage('name', $this->buildNamePage())
           ->addPage('remote-uri', $this->buildRemoteURIPage())
           ->addPage('auth', $this->buildAuthPage())
           ->addPage('policy', $this->buildPolicyPage())
           ->addPage('done', $this->buildDonePage());
         break;
     }
 
     if ($request->isFormPost()) {
       $form->readFromRequest($request);
       if ($form->isComplete()) {
 
         $is_create = ($this->edit === 'import' || $this->edit === 'create');
         $is_auth = ($this->edit == 'import' || $this->edit == 'remote');
         $is_policy = ($this->edit != 'remote');
         $is_init = ($this->edit == 'create');
 
         if ($is_create) {
           $repository = PhabricatorRepository::initializeNewRepository(
             $viewer);
         }
 
         $template = id(new PhabricatorRepositoryTransaction());
 
         $type_name = PhabricatorRepositoryTransaction::TYPE_NAME;
         $type_vcs = PhabricatorRepositoryTransaction::TYPE_VCS;
         $type_activate = PhabricatorRepositoryTransaction::TYPE_ACTIVATE;
         $type_local_path = PhabricatorRepositoryTransaction::TYPE_LOCAL_PATH;
         $type_remote_uri = PhabricatorRepositoryTransaction::TYPE_REMOTE_URI;
         $type_hosting = PhabricatorRepositoryTransaction::TYPE_HOSTING;
         $type_http = PhabricatorRepositoryTransaction::TYPE_PROTOCOL_HTTP;
         $type_ssh = PhabricatorRepositoryTransaction::TYPE_PROTOCOL_SSH;
         $type_credential = PhabricatorRepositoryTransaction::TYPE_CREDENTIAL;
         $type_view = PhabricatorTransactions::TYPE_VIEW_POLICY;
         $type_edit = PhabricatorTransactions::TYPE_EDIT_POLICY;
         $type_push = PhabricatorRepositoryTransaction::TYPE_PUSH_POLICY;
         $type_service = PhabricatorRepositoryTransaction::TYPE_SERVICE;
 
         $xactions = array();
 
         // If we're creating a new repository, set all this core stuff.
         if ($is_create) {
           $callsign = $form->getPage('name')
             ->getControl('callsign')->getValue();
 
           // We must set this to a unique value to save the repository
           // initially, and it's immutable, so we don't bother using
           // transactions to apply this change.
           $repository->setCallsign($callsign);
 
           $xactions[] = id(clone $template)
             ->setTransactionType($type_name)
             ->setNewValue(
               $form->getPage('name')->getControl('name')->getValue());
 
           $xactions[] = id(clone $template)
             ->setTransactionType($type_vcs)
             ->setNewValue(
               $form->getPage('vcs')->getControl('vcs')->getValue());
 
           $activate = $form->getPage('done')
             ->getControl('activate')->getValue();
           $xactions[] = id(clone $template)
             ->setTransactionType($type_activate)
             ->setNewValue(($activate == 'start'));
 
           if ($service) {
             $xactions[] = id(clone $template)
               ->setTransactionType($type_service)
               ->setNewValue($service->getPHID());
           }
 
           $default_local_path = PhabricatorEnv::getEnvConfig(
             'repository.default-local-path');
 
           $default_local_path = rtrim($default_local_path, '/');
           $default_local_path = $default_local_path.'/'.$callsign.'/';
 
           $xactions[] = id(clone $template)
             ->setTransactionType($type_local_path)
             ->setNewValue($default_local_path);
         }
 
         if ($is_init) {
           $xactions[] = id(clone $template)
             ->setTransactionType($type_hosting)
             ->setNewValue(true);
           $vcs = $form->getPage('vcs')->getControl('vcs')->getValue();
           if ($vcs != PhabricatorRepositoryType::REPOSITORY_TYPE_SVN) {
             if (PhabricatorEnv::getEnvConfig('diffusion.allow-http-auth')) {
               $v_http_mode = PhabricatorRepository::SERVE_READWRITE;
             } else {
               $v_http_mode = PhabricatorRepository::SERVE_OFF;
             }
             $xactions[] = id(clone $template)
               ->setTransactionType($type_http)
               ->setNewValue($v_http_mode);
           }
 
           if (PhabricatorEnv::getEnvConfig('diffusion.ssh-user')) {
             $v_ssh_mode = PhabricatorRepository::SERVE_READWRITE;
           } else {
             $v_ssh_mode = PhabricatorRepository::SERVE_OFF;
           }
           $xactions[] = id(clone $template)
             ->setTransactionType($type_ssh)
             ->setNewValue($v_ssh_mode);
         }
 
         if ($is_auth) {
           $xactions[] = id(clone $template)
             ->setTransactionType($type_remote_uri)
             ->setNewValue(
               $form->getPage('remote-uri')->getControl('remoteURI')
                 ->getValue());
 
           $xactions[] = id(clone $template)
             ->setTransactionType($type_credential)
             ->setNewValue(
               $form->getPage('auth')->getControl('credential')->getValue());
         }
 
         if ($is_policy) {
           $xactions[] = id(clone $template)
             ->setTransactionType($type_view)
             ->setNewValue(
               $form->getPage('policy')->getControl('viewPolicy')->getValue());
 
           $xactions[] = id(clone $template)
             ->setTransactionType($type_edit)
             ->setNewValue(
               $form->getPage('policy')->getControl('editPolicy')->getValue());
 
           if ($is_init || $repository->isHosted()) {
             $xactions[] = id(clone $template)
               ->setTransactionType($type_push)
               ->setNewValue(
                 $form->getPage('policy')->getControl('pushPolicy')->getValue());
           }
         }
 
         id(new PhabricatorRepositoryEditor())
           ->setContinueOnNoEffect(true)
           ->setContentSourceFromRequest($request)
           ->setActor($viewer)
           ->applyTransactions($repository, $xactions);
 
         $repo_uri = $this->getRepositoryControllerURI($repository, 'edit/');
         return id(new AphrontRedirectResponse())->setURI($repo_uri);
       }
     } else {
       $dict = array();
       if ($repository) {
         $dict = array(
           'remoteURI' => $repository->getRemoteURI(),
           'credential' => $repository->getCredentialPHID(),
           'viewPolicy' => $repository->getViewPolicy(),
           'editPolicy' => $repository->getEditPolicy(),
           'pushPolicy' => $repository->getPushPolicy(),
         );
       }
       $form->readFromObject($dict);
     }
 
     $crumbs = $this->buildApplicationCrumbs();
     $crumbs->addTextCrumb($title);
 
     return $this->buildApplicationPage(
       array(
         $crumbs,
         $form,
       ),
       array(
         'title' => $title,
       ));
   }
 
 
 /* -(  Page: VCS Type  )----------------------------------------------------- */
 
 
   private function buildVCSPage() {
 
     $is_import = ($this->edit == 'import');
 
     if ($is_import) {
       $git_str = pht(
         'Import a Git repository (for example, a repository hosted '.
         'on GitHub).');
       $hg_str = pht(
         'Import a Mercurial repository (for example, a repository '.
         'hosted on Bitbucket).');
       $svn_str = pht('Import a Subversion repository.');
     } else {
       $git_str = pht('Create a new, empty Git repository.');
       $hg_str = pht('Create a new, empty Mercurial repository.');
       $svn_str = pht('Create a new, empty Subversion repository.');
     }
 
     $control = id(new AphrontFormRadioButtonControl())
       ->setName('vcs')
       ->setLabel(pht('Type'))
       ->addButton(
         PhabricatorRepositoryType::REPOSITORY_TYPE_GIT,
         pht('Git'),
         $git_str)
       ->addButton(
         PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL,
         pht('Mercurial'),
         $hg_str)
       ->addButton(
         PhabricatorRepositoryType::REPOSITORY_TYPE_SVN,
         pht('Subversion'),
         $svn_str);
 
     if ($is_import) {
       $control->addButton(
         PhabricatorRepositoryType::REPOSITORY_TYPE_PERFORCE,
         pht('Perforce'),
         pht(
           'Perforce is not directly supported, but you can import '.
           'a Perforce repository as a Git repository using %s.',
           phutil_tag(
             'a',
             array(
               'href' =>
                 'http://www.perforce.com/product/components/git-fusion',
               'target' => '_blank',
             ),
             pht('Perforce Git Fusion'))),
         'disabled',
         $disabled = true);
     }
 
     return id(new PHUIFormPageView())
       ->setPageName(pht('Repository Type'))
       ->setUser($this->getRequest()->getUser())
       ->setValidateFormPageCallback(array($this, 'validateVCSPage'))
       ->addControl($control);
   }
 
   public function validateVCSPage(PHUIFormPageView $page) {
     $valid = array(
       PhabricatorRepositoryType::REPOSITORY_TYPE_GIT => true,
       PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL => true,
       PhabricatorRepositoryType::REPOSITORY_TYPE_SVN => true,
     );
 
     $c_vcs = $page->getControl('vcs');
     $v_vcs = $c_vcs->getValue();
     if (!$v_vcs) {
       $c_vcs->setError(pht('Required'));
       $page->addPageError(
         pht('You must select a version control system.'));
     } else if (empty($valid[$v_vcs])) {
       $c_vcs->setError(pht('Invalid'));
       $page->addPageError(
         pht('You must select a valid version control system.'));
     }
 
     return $c_vcs->isValid();
   }
 
 
 /* -(  Page: Name and Callsign  )-------------------------------------------- */
 
 
   private function buildNamePage() {
     return id(new PHUIFormPageView())
       ->setUser($this->getRequest()->getUser())
       ->setPageName(pht('Repository Name and Location'))
       ->setValidateFormPageCallback(array($this, 'validateNamePage'))
       ->addRemarkupInstructions(
         pht(
           '**Choose a human-readable name for this repository**, like '.
           '"CompanyName Mobile App" or "CompanyName Backend Server". You '.
           'can change this later.'))
       ->addControl(
         id(new AphrontFormTextControl())
           ->setName('name')
           ->setLabel(pht('Name'))
           ->setCaption(pht('Human-readable repository name.')))
       ->addRemarkupInstructions(
         pht(
           '**Choose a "Callsign" for the repository.** This is a short, '.
           'unique string which identifies commits elsewhere in Phabricator. '.
           'For example, you might use `M` for your mobile app repository '.
           'and `B` for your backend repository.'.
           "\n\n".
           '**Callsigns must be UPPERCASE**, and can not be edited after the '.
           'repository is created. Generally, you should choose short '.
           'callsigns.'))
       ->addControl(
         id(new AphrontFormTextControl())
           ->setName('callsign')
           ->setLabel(pht('Callsign'))
           ->setCaption(pht('Short UPPERCASE identifier.')));
   }
 
   public function validateNamePage(PHUIFormPageView $page) {
     $c_name = $page->getControl('name');
     $v_name = $c_name->getValue();
     if (!strlen($v_name)) {
       $c_name->setError(pht('Required'));
       $page->addPageError(
         pht('You must choose a name for this repository.'));
     }
 
     $c_call = $page->getControl('callsign');
     $v_call = $c_call->getValue();
     if (!strlen($v_call)) {
       $c_call->setError(pht('Required'));
       $page->addPageError(
         pht('You must choose a callsign for this repository.'));
     } else if (!preg_match('/^[A-Z]+\z/', $v_call)) {
       $c_call->setError(pht('Invalid'));
       $page->addPageError(
         pht('The callsign must contain only UPPERCASE letters.'));
     } else {
       $exists = false;
       try {
         $repo = id(new PhabricatorRepositoryQuery())
           ->setViewer($this->getRequest()->getUser())
           ->withCallsigns(array($v_call))
           ->executeOne();
         $exists = (bool)$repo;
       } catch (PhabricatorPolicyException $ex) {
         $exists = true;
       }
       if ($exists) {
         $c_call->setError(pht('Not Unique'));
         $page->addPageError(
           pht(
             'Another repository already uses that callsign. You must choose '.
             'a unique callsign.'));
       }
     }
 
     return $c_name->isValid() &&
            $c_call->isValid();
   }
 
 
 /* -(  Page: Remote URI  )--------------------------------------------------- */
 
 
   private function buildRemoteURIPage() {
     return id(new PHUIFormPageView())
       ->setUser($this->getRequest()->getUser())
       ->setPageName(pht('Repository Remote URI'))
       ->setValidateFormPageCallback(array($this, 'validateRemoteURIPage'))
       ->setAdjustFormPageCallback(array($this, 'adjustRemoteURIPage'))
       ->addControl(
         id(new AphrontFormTextControl())
           ->setName('remoteURI'));
   }
 
   public function adjustRemoteURIPage(PHUIFormPageView $page) {
     $form = $page->getForm();
 
     $is_git = false;
     $is_svn = false;
     $is_mercurial = false;
 
     if ($this->getRepository()) {
       $vcs = $this->getRepository()->getVersionControlSystem();
     } else {
       $vcs = $form->getPage('vcs')->getControl('vcs')->getValue();
     }
 
     switch ($vcs) {
       case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
         $is_git = true;
         break;
       case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
         $is_svn = true;
         break;
       case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
         $is_mercurial = true;
         break;
       default:
         throw new Exception(pht('Unsupported VCS!'));
     }
 
     $has_local = ($is_git || $is_mercurial);
     if ($is_git) {
       $uri_label = pht('Remote URI');
       $instructions = pht(
         'Enter the URI to clone this Git repository from. It should usually '.
         'look like one of these examples:'.
         "\n\n".
         "| Example Git Remote URIs |\n".
         "| ----------------------- |\n".
         "| `git@github.com:example/example.git` |\n".
         "| `ssh://user@host.com/git/example.git` |\n".
         "| `https://example.com/repository.git` |\n");
     } else if ($is_mercurial) {
       $uri_label = pht('Remote URI');
       $instructions = pht(
         'Enter the URI to clone this Mercurial repository from. It should '.
         'usually look like one of these examples:'.
         "\n\n".
         "| Example Mercurial Remote URIs |\n".
         "| ----------------------- |\n".
         "| `ssh://hg@bitbucket.org/example/repository` |\n".
         "| `https://bitbucket.org/example/repository` |\n");
     } else if ($is_svn) {
       $uri_label = pht('Repository Root');
       $instructions = pht(
         'Enter the **Repository Root** for this Subversion repository. '.
         'You can figure this out by running `svn info` in a working copy '.
         'and looking at the value in the `Repository Root` field. It '.
         'should be a URI and will usually look like these:'.
         "\n\n".
         "| Example Subversion Repository Root URIs |\n".
         "| ------------------------------ |\n".
         "| `http://svn.example.org/svnroot/` |\n".
         "| `svn+ssh://svn.example.com/svnroot/` |\n".
         "| `svn://svn.example.net/svnroot/` |\n".
         "\n\n".
         "You **MUST** specify the root of the repository, not a ".
         "subdirectory. (If you want to import only part of a Subversion ".
         "repository, use the //Import Only// option at the end of this ".
         "workflow.)");
     } else {
       throw new Exception(pht('Unsupported VCS!'));
     }
 
     $page->addRemarkupInstructions($instructions, 'remoteURI');
     $page->getControl('remoteURI')->setLabel($uri_label);
   }
 
   public function validateRemoteURIPage(PHUIFormPageView $page) {
     $c_remote = $page->getControl('remoteURI');
     $v_remote = $c_remote->getValue();
 
     if (!strlen($v_remote)) {
       $c_remote->setError(pht('Required'));
       $page->addPageError(
         pht('You must specify a URI.'));
     } else {
       try {
         PhabricatorRepository::assertValidRemoteURI($v_remote);
       } catch (Exception $ex) {
         $c_remote->setError(pht('Invalid'));
         $page->addPageError($ex->getMessage());
       }
     }
 
     return $c_remote->isValid();
   }
 
 
 /* -(  Page: Authentication  )----------------------------------------------- */
 
 
   public function buildAuthPage() {
     return id(new PHUIFormPageView())
       ->setPageName(pht('Authentication'))
       ->setUser($this->getRequest()->getUser())
       ->setAdjustFormPageCallback(array($this, 'adjustAuthPage'))
       ->addControl(
         id(new PassphraseCredentialControl())
           ->setName('credential'));
   }
 
 
   public function adjustAuthPage($page) {
     $form = $page->getForm();
 
     if ($this->getRepository()) {
       $vcs = $this->getRepository()->getVersionControlSystem();
     } else {
       $vcs = $form->getPage('vcs')->getControl('vcs')->getValue();
     }
 
     $remote_uri = $form->getPage('remote-uri')
       ->getControl('remoteURI')
       ->getValue();
 
     $proto = PhabricatorRepository::getRemoteURIProtocol($remote_uri);
     $remote_user = $this->getRemoteURIUser($remote_uri);
 
     $c_credential = $page->getControl('credential');
     $c_credential->setDefaultUsername($remote_user);
 
     if ($this->isSSHProtocol($proto)) {
       $c_credential->setLabel(pht('SSH Key'));
       $c_credential->setCredentialType(
-        PassphraseCredentialTypeSSHPrivateKeyText::CREDENTIAL_TYPE);
-      $provides_type = PassphraseCredentialTypeSSHPrivateKey::PROVIDES_TYPE;
+        PassphraseSSHPrivateKeyTextCredentialType::CREDENTIAL_TYPE);
+      $provides_type = PassphraseSSHPrivateKeyCredentialType::PROVIDES_TYPE;
 
       $page->addRemarkupInstructions(
         pht(
           'Choose or add the SSH credentials to use to connect to the the '.
           'repository hosted at:'.
           "\n\n".
           "  lang=text\n".
           "  %s",
           $remote_uri),
         'credential');
     } else if ($this->isUsernamePasswordProtocol($proto)) {
       $c_credential->setLabel(pht('Password'));
       $c_credential->setAllowNull(true);
       $c_credential->setCredentialType(
-        PassphraseCredentialTypePassword::CREDENTIAL_TYPE);
-      $provides_type = PassphraseCredentialTypePassword::PROVIDES_TYPE;
+        PassphrasePasswordCredentialType::CREDENTIAL_TYPE);
+      $provides_type = PassphrasePasswordCredentialType::PROVIDES_TYPE;
 
       $page->addRemarkupInstructions(
         pht(
           'Choose the username and password used to connect to the '.
           'repository hosted at:'.
           "\n\n".
           "  lang=text\n".
           "  %s".
           "\n\n".
           "If this repository does not require a username or password, ".
           "you can continue to the next step.",
           $remote_uri),
         'credential');
     } else {
       throw new Exception(pht('Unknown URI protocol!'));
     }
 
     if ($provides_type) {
       $viewer = $this->getRequest()->getUser();
 
       $options = id(new PassphraseCredentialQuery())
         ->setViewer($viewer)
         ->withIsDestroyed(false)
         ->withProvidesTypes(array($provides_type))
         ->execute();
 
       $c_credential->setOptions($options);
     }
 
   }
 
   public function validateAuthPage(PHUIFormPageView $page) {
     $form = $page->getForm();
     $remote_uri = $form->getPage('remote')->getControl('remoteURI')->getValue();
     $proto = $this->getRemoteURIProtocol($remote_uri);
 
     $c_credential = $page->getControl('credential');
     $v_credential = $c_credential->getValue();
 
     // NOTE: We're using the omnipotent user here because the viewer might be
     // editing a repository they're allowed to edit which uses a credential they
     // are not allowed to see. This is fine, as long as they don't change it.
     $credential = id(new PassphraseCredentialQuery())
       ->setViewer(PhabricatorUser::getOmnipotentUser())
       ->withPHIDs(array($v_credential))
       ->executeOne();
 
     if ($this->isSSHProtocol($proto)) {
       if (!$credential) {
         $c_credential->setError(pht('Required'));
         $page->addPageError(
           pht('You must choose an SSH credential to connect over SSH.'));
       }
 
-      $ssh_type = PassphraseCredentialTypeSSHPrivateKey::PROVIDES_TYPE;
+      $ssh_type = PassphraseSSHPrivateKeyCredentialType::PROVIDES_TYPE;
       if ($credential->getProvidesType() !== $ssh_type) {
         $c_credential->setError(pht('Invalid'));
         $page->addPageError(
           pht(
             'You must choose an SSH credential, not some other type '.
             'of credential.'));
       }
 
     } else if ($this->isUsernamePasswordProtocol($proto)) {
       if ($credential) {
-        $password_type = PassphraseCredentialTypePassword::PROVIDES_TYPE;
+        $password_type = PassphrasePasswordCredentialType::PROVIDES_TYPE;
         if ($credential->getProvidesType() !== $password_type) {
         $c_credential->setError(pht('Invalid'));
         $page->addPageError(
           pht(
             'You must choose a username/password credential, not some other '.
             'type of credential.'));
         }
       }
 
       return $c_credential->isValid();
     } else {
       return true;
     }
   }
 
 
 /* -(  Page: Policy  )------------------------------------------------------- */
 
 
   private function buildPolicyPage() {
     $viewer = $this->getRequest()->getUser();
     if ($this->getRepository()) {
       $repository = $this->getRepository();
     } else {
       $repository = PhabricatorRepository::initializeNewRepository($viewer);
     }
 
     $policies = id(new PhabricatorPolicyQuery())
       ->setViewer($viewer)
       ->setObject($repository)
       ->execute();
 
     $view_policy = id(new AphrontFormPolicyControl())
       ->setUser($viewer)
       ->setCapability(PhabricatorPolicyCapability::CAN_VIEW)
       ->setPolicyObject($repository)
       ->setPolicies($policies)
       ->setName('viewPolicy');
 
     $edit_policy = id(new AphrontFormPolicyControl())
       ->setUser($viewer)
       ->setCapability(PhabricatorPolicyCapability::CAN_EDIT)
       ->setPolicyObject($repository)
       ->setPolicies($policies)
       ->setName('editPolicy');
 
     $push_policy = id(new AphrontFormPolicyControl())
       ->setUser($viewer)
       ->setCapability(DiffusionPushCapability::CAPABILITY)
       ->setPolicyObject($repository)
       ->setPolicies($policies)
       ->setName('pushPolicy');
 
     return id(new PHUIFormPageView())
         ->setPageName(pht('Policies'))
         ->setValidateFormPageCallback(array($this, 'validatePolicyPage'))
         ->setAdjustFormPageCallback(array($this, 'adjustPolicyPage'))
         ->setUser($viewer)
         ->addRemarkupInstructions(
           pht('Select access policies for this repository.'))
         ->addControl($view_policy)
         ->addControl($edit_policy)
         ->addControl($push_policy);
   }
 
   public function adjustPolicyPage(PHUIFormPageView $page) {
     if ($this->getRepository()) {
       $repository = $this->getRepository();
       $show_push = $repository->isHosted();
     } else {
       $show_push = ($this->edit == 'create');
     }
 
     if (!$show_push) {
       $c_push = $page->getControl('pushPolicy');
       $c_push->setHidden(true);
     }
   }
 
   public function validatePolicyPage(PHUIFormPageView $page) {
     $form = $page->getForm();
     $viewer = $this->getRequest()->getUser();
 
     $c_view = $page->getControl('viewPolicy');
     $c_edit = $page->getControl('editPolicy');
     $c_push = $page->getControl('pushPolicy');
     $v_view = $c_view->getValue();
     $v_edit = $c_edit->getValue();
     $v_push = $c_push->getValue();
 
     if ($this->getRepository()) {
       $repository = $this->getRepository();
     } else {
       $repository = PhabricatorRepository::initializeNewRepository($viewer);
     }
 
     $proxy = clone $repository;
     $proxy->setViewPolicy($v_view);
     $proxy->setEditPolicy($v_edit);
 
     $can_view = PhabricatorPolicyFilter::hasCapability(
       $viewer,
       $proxy,
       PhabricatorPolicyCapability::CAN_VIEW);
 
     $can_edit = PhabricatorPolicyFilter::hasCapability(
       $viewer,
       $proxy,
       PhabricatorPolicyCapability::CAN_EDIT);
 
     if (!$can_view) {
       $c_view->setError(pht('Invalid'));
       $page->addPageError(
         pht(
           'You can not use the selected policy, because you would be unable '.
           'to see the repository.'));
     }
 
     if (!$can_edit) {
       $c_edit->setError(pht('Invalid'));
       $page->addPageError(
         pht(
           'You can not use the selected edit policy, because you would be '.
           'unable to edit the repository.'));
     }
 
     return $c_view->isValid() &&
            $c_edit->isValid();
   }
 
 /* -(  Page: Done  )--------------------------------------------------------- */
 
 
   private function buildDonePage() {
 
     $is_create = ($this->edit == 'create');
     if ($is_create) {
       $now_label = pht('Create Repository Now');
       $now_caption = pht(
         'Create the repository right away. This will create the repository '.
         'using default settings.');
 
       $wait_label = pht('Configure More Options First');
       $wait_caption = pht(
         'Configure more options before creating the repository. '.
         'This will let you fine-tune settings. You can create the repository '.
         'whenever you are ready.');
     } else {
       $now_label = pht('Start Import Now');
       $now_caption = pht(
         'Start importing the repository right away. This will import '.
         'the entire repository using default settings.');
 
       $wait_label = pht('Configure More Options First');
       $wait_caption = pht(
         'Configure more options before beginning the repository '.
         'import. This will let you fine-tune settings. You can '.
         'start the import whenever you are ready.');
     }
 
     return id(new PHUIFormPageView())
       ->setPageName(pht('Repository Ready!'))
       ->setValidateFormPageCallback(array($this, 'validateDonePage'))
       ->setUser($this->getRequest()->getUser())
       ->addControl(
         id(new AphrontFormRadioButtonControl())
           ->setName('activate')
           ->setLabel(pht('Start Now'))
           ->addButton(
             'start',
             $now_label,
             $now_caption)
           ->addButton(
             'wait',
             $wait_label,
             $wait_caption));
   }
 
   public function validateDonePage(PHUIFormPageView $page) {
     $c_activate = $page->getControl('activate');
     $v_activate = $c_activate->getValue();
 
     if ($v_activate != 'start' && $v_activate != 'wait') {
       $c_activate->setError(pht('Required'));
       $page->addPageError(
         pht('Make a choice about repository activation.'));
     }
 
     return $c_activate->isValid();
   }
 
 
 /* -(  Internal  )----------------------------------------------------------- */
 
   private function getRemoteURIUser($raw_uri) {
     $uri = new PhutilURI($raw_uri);
     if ($uri->getUser()) {
       return $uri->getUser();
     }
 
     $git_uri = new PhutilGitURI($raw_uri);
     if (strlen($git_uri->getDomain()) && strlen($git_uri->getPath())) {
       return $git_uri->getUser();
     }
 
     return null;
   }
 
   private function isSSHProtocol($proto) {
     return ($proto == 'git' || $proto == 'ssh' || $proto == 'svn+ssh');
   }
 
   private function isUsernamePasswordProtocol($proto) {
     return ($proto == 'http' || $proto == 'https' || $proto == 'svn');
   }
 
   private function setRepository(PhabricatorRepository $repository) {
     $this->repository = $repository;
     return $this;
   }
 
   private function getRepository() {
     return $this->repository;
   }
 
 }
diff --git a/src/applications/diffusion/data/DiffusionBrowseResultSet.php b/src/applications/diffusion/data/DiffusionBrowseResultSet.php
index 4c2d8a1b8..2208aca50 100644
--- a/src/applications/diffusion/data/DiffusionBrowseResultSet.php
+++ b/src/applications/diffusion/data/DiffusionBrowseResultSet.php
@@ -1,162 +1,162 @@
 <?php
 
-final class DiffusionBrowseResultSet {
+final class DiffusionBrowseResultSet extends Phobject {
 
   const REASON_IS_FILE              = 'is-file';
   const REASON_IS_DELETED           = 'is-deleted';
   const REASON_IS_NONEXISTENT       = 'nonexistent';
   const REASON_BAD_COMMIT           = 'bad-commit';
   const REASON_IS_EMPTY             = 'empty';
   const REASON_IS_UNTRACKED_PARENT  = 'untracked-parent';
 
   private $paths;
   private $isValidResults;
   private $reasonForEmptyResultSet;
   private $existedAtCommit;
   private $deletedAtCommit;
 
   public function setPaths(array $paths) {
     assert_instances_of($paths, 'DiffusionRepositoryPath');
     $this->paths = $paths;
     return $this;
   }
   public function getPaths() {
     return $this->paths;
   }
 
   public function setIsValidResults($is_valid) {
     $this->isValidResults = $is_valid;
     return $this;
   }
   public function isValidResults() {
     return $this->isValidResults;
   }
 
   public function setReasonForEmptyResultSet($reason) {
     $this->reasonForEmptyResultSet = $reason;
     return $this;
   }
   public function getReasonForEmptyResultSet() {
     return $this->reasonForEmptyResultSet;
   }
 
   public function setExistedAtCommit($existed_at_commit) {
     $this->existedAtCommit = $existed_at_commit;
     return $this;
   }
   public function getExistedAtCommit() {
     return $this->existedAtCommit;
   }
 
   public function setDeletedAtCommit($deleted_at_commit) {
     $this->deletedAtCommit = $deleted_at_commit;
     return $this;
   }
   public function getDeletedAtCommit() {
     return $this->deletedAtCommit;
   }
 
   public function toDictionary() {
     $paths = $this->getPathDicts();
 
     return array(
       'paths' => $paths,
       'isValidResults' => $this->isValidResults(),
       'reasonForEmptyResultSet' => $this->getReasonForEmptyResultSet(),
       'existedAtCommit' => $this->getExistedAtCommit(),
       'deletedAtCommit' => $this->getDeletedAtCommit(),
     );
   }
 
   public function getPathDicts() {
     $paths = $this->getPaths();
     if ($paths) {
       return mpull($paths, 'toDictionary');
     }
     return array();
   }
 
   /**
    * Get the best README file in this result set, if one exists.
    *
    * Callers should normally use `diffusion.filecontentquery` to pull README
    * content.
    *
    * @return string|null Full path to best README, or null if one does not
    *   exist.
    */
   public function getReadmePath() {
     $allowed_types = array(
       ArcanistDiffChangeType::FILE_NORMAL => true,
       ArcanistDiffChangeType::FILE_TEXT => true,
     );
 
     $candidates = array();
     foreach ($this->getPaths() as $path_object) {
       if (empty($allowed_types[$path_object->getFileType()])) {
         // Skip directories, images, etc.
         continue;
       }
 
       $local_path = $path_object->getPath();
       if (!preg_match('/^readme(\.|$)/i', $local_path)) {
         // Skip files not named "README".
         continue;
       }
 
       $full_path = $path_object->getFullPath();
       $candidates[$full_path] = self::getReadmePriority($local_path);
     }
 
     if (!$candidates) {
       return null;
     }
 
     arsort($candidates);
     return head_key($candidates);
   }
 
   /**
    * Get the priority of a README file.
    *
    * When a directory contains several README files, this function scores them
    * so the caller can select a preferred file. See @{method:getReadmePath}.
    *
    * @param string Local README path, like "README.txt".
    * @return int Priority score, with higher being more preferred.
    */
   public static function getReadmePriority($path) {
     $path = phutil_utf8_strtolower($path);
     if ($path == 'readme') {
       return 90;
     }
 
     $ext = last(explode('.', $path));
     switch ($ext) {
       case 'remarkup':
         return 100;
       case 'rainbow':
         return 80;
       case 'md':
         return 70;
       case 'txt':
         return 60;
       default:
         return 50;
     }
   }
 
   public static function newFromConduit(array $data) {
     $paths = array();
     $path_dicts = $data['paths'];
     foreach ($path_dicts as $dict) {
       $paths[] = DiffusionRepositoryPath::newFromDictionary($dict);
     }
     return id(new DiffusionBrowseResultSet())
       ->setPaths($paths)
       ->setIsValidResults($data['isValidResults'])
       ->setReasonForEmptyResultSet($data['reasonForEmptyResultSet'])
       ->setExistedAtCommit($data['existedAtCommit'])
       ->setDeletedAtCommit($data['deletedAtCommit']);
   }
 }
diff --git a/src/applications/diffusion/data/DiffusionFileContent.php b/src/applications/diffusion/data/DiffusionFileContent.php
index 3919d30ea..1680d1f96 100644
--- a/src/applications/diffusion/data/DiffusionFileContent.php
+++ b/src/applications/diffusion/data/DiffusionFileContent.php
@@ -1,63 +1,63 @@
 <?php
 
-final class DiffusionFileContent {
+final class DiffusionFileContent extends Phobject {
 
   private $corpus;
   private $blameDict;
   private $revList;
   private $textList;
 
   public function setTextList(array $text_list) {
     $this->textList = $text_list;
     return $this;
   }
   public function getTextList() {
     if (!$this->textList) {
       return phutil_split_lines($this->getCorpus(), $retain_ends = false);
     }
     return $this->textList;
   }
 
   public function setRevList(array $rev_list) {
     $this->revList = $rev_list;
     return $this;
   }
   public function getRevList() {
     return $this->revList;
   }
 
   public function setBlameDict(array $blame_dict) {
     $this->blameDict = $blame_dict;
     return $this;
   }
   public function getBlameDict() {
     return $this->blameDict;
   }
 
   public function setCorpus($corpus) {
     $this->corpus = $corpus;
     return $this;
   }
 
   public function getCorpus() {
     return $this->corpus;
   }
 
   public function toDictionary() {
     return array(
       'corpus' => $this->getCorpus(),
       'blameDict' => $this->getBlameDict(),
       'revList' => $this->getRevList(),
       'textList' => $this->getTextList(),
     );
   }
 
   public static function newFromConduit(array $dict) {
     return id(new DiffusionFileContent())
       ->setCorpus($dict['corpus'])
       ->setBlameDict($dict['blameDict'])
       ->setRevList($dict['revList'])
       ->setTextList($dict['textList']);
   }
 
 }
diff --git a/src/applications/diffusion/data/DiffusionGitBranch.php b/src/applications/diffusion/data/DiffusionGitBranch.php
index 8f69645b7..7fe8f5c6c 100644
--- a/src/applications/diffusion/data/DiffusionGitBranch.php
+++ b/src/applications/diffusion/data/DiffusionGitBranch.php
@@ -1,110 +1,110 @@
 <?php
 
-final class DiffusionGitBranch {
+final class DiffusionGitBranch extends Phobject {
 
   const DEFAULT_GIT_REMOTE = 'origin';
 
   /**
    * Parse the output of 'git branch -r --verbose --no-abbrev' or similar into
    * a map. For instance:
    *
    *   array(
    *     'origin/master' => '99a9c082f9a1b68c7264e26b9e552484a5ae5f25',
    *   );
    *
    * If you specify $only_this_remote, branches will be filtered to only those
    * on the given remote, **and the remote name will be stripped**. For example:
    *
    *   array(
    *     'master' => '99a9c082f9a1b68c7264e26b9e552484a5ae5f25',
    *   );
    *
    * @param string stdout of git branch command.
    * @param string Filter branches to those on a specific remote.
    * @return map Map of 'branch' or 'remote/branch' to hash at HEAD.
    */
   public static function parseRemoteBranchOutput(
     $stdout,
     $only_this_remote = null) {
     $map = array();
 
     $lines = array_filter(explode("\n", $stdout));
     foreach ($lines as $line) {
       $matches = null;
       if (preg_match('/^  (\S+)\s+-> (\S+)$/', $line, $matches)) {
           // This is a line like:
           //
           //   origin/HEAD          -> origin/master
           //
           // ...which we don't currently do anything interesting with, although
           // in theory we could use it to automatically choose the default
           // branch.
           continue;
       }
       if (!preg_match('/^ *(\S+)\s+([a-z0-9]{40})/', $line, $matches)) {
         throw new Exception(
           pht(
             'Failed to parse %s!',
             $line));
       }
 
       $remote_branch = $matches[1];
       $branch_head = $matches[2];
 
       if (strpos($remote_branch, 'HEAD') !== false) {
         // let's assume that no one will call their remote or branch HEAD
         continue;
       }
 
       if ($only_this_remote) {
         $matches = null;
         if (!preg_match('#^([^/]+)/(.*)$#', $remote_branch, $matches)) {
           throw new Exception(
             pht(
               "Failed to parse remote branch '%s'!",
               $remote_branch));
         }
         $remote_name = $matches[1];
         $branch_name = $matches[2];
         if ($remote_name != $only_this_remote) {
           continue;
         }
         $map[$branch_name] = $branch_head;
       } else {
         $map[$remote_branch] = $branch_head;
       }
     }
 
     return $map;
   }
 
   /**
    * As above, but with no `-r`. Used for bare repositories.
    */
   public static function parseLocalBranchOutput($stdout) {
     $map = array();
 
     $lines = array_filter(explode("\n", $stdout));
     $regex = '/^[* ]*(\(no branch\)|\S+)\s+([a-z0-9]{40})/';
     foreach ($lines as $line) {
       $matches = null;
       if (!preg_match($regex, $line, $matches)) {
         throw new Exception(
           pht(
             'Failed to parse %s!',
             $line));
       }
 
       $branch = $matches[1];
       $branch_head = $matches[2];
       if ($branch == '(no branch)') {
         continue;
       }
 
       $map[$branch] = $branch_head;
     }
 
     return $map;
   }
 
 }
diff --git a/src/applications/diffusion/data/DiffusionPathChange.php b/src/applications/diffusion/data/DiffusionPathChange.php
index 939fea944..be21125e6 100644
--- a/src/applications/diffusion/data/DiffusionPathChange.php
+++ b/src/applications/diffusion/data/DiffusionPathChange.php
@@ -1,202 +1,202 @@
 <?php
 
-final class DiffusionPathChange {
+final class DiffusionPathChange extends Phobject {
 
   private $path;
   private $commitIdentifier;
   private $commit;
   private $commitData;
 
   private $changeType;
   private $fileType;
   private $targetPath;
   private $targetCommitIdentifier;
   private $awayPaths = array();
 
   public function setPath($path) {
     $this->path = $path;
     return $this;
   }
 
   public function getPath() {
     return $this->path;
   }
 
   public function setChangeType($change_type) {
     $this->changeType = $change_type;
     return $this;
   }
 
   public function getChangeType() {
     return $this->changeType;
   }
 
   public function setFileType($file_type) {
     $this->fileType = $file_type;
     return $this;
   }
 
   public function getFileType() {
     return $this->fileType;
   }
 
   public function setTargetPath($target_path) {
     $this->targetPath = $target_path;
     return $this;
   }
 
   public function getTargetPath() {
     return $this->targetPath;
   }
 
   public function setAwayPaths(array $away_paths) {
     $this->awayPaths = $away_paths;
     return $this;
   }
 
   public function getAwayPaths() {
     return $this->awayPaths;
   }
 
   public function setCommitIdentifier($commit) {
     $this->commitIdentifier = $commit;
     return $this;
   }
 
   public function getCommitIdentifier() {
     return $this->commitIdentifier;
   }
 
   public function setTargetCommitIdentifier($target_commit_identifier) {
     $this->targetCommitIdentifier = $target_commit_identifier;
     return $this;
   }
 
   public function getTargetCommitIdentifier() {
     return $this->targetCommitIdentifier;
   }
 
   public function setCommit($commit) {
     $this->commit = $commit;
     return $this;
   }
 
   public function getCommit() {
     return $this->commit;
   }
 
   public function setCommitData($commit_data) {
     $this->commitData = $commit_data;
     return $this;
   }
 
   public function getCommitData() {
     return $this->commitData;
   }
 
 
   public function getEpoch() {
     if ($this->getCommit()) {
       return $this->getCommit()->getEpoch();
     }
     return null;
   }
 
   public function getAuthorName() {
     if ($this->getCommitData()) {
       return $this->getCommitData()->getAuthorName();
     }
     return null;
   }
 
   public function getSummary() {
     if (!$this->getCommitData()) {
       return null;
     }
     $message = $this->getCommitData()->getCommitMessage();
     $first = idx(explode("\n", $message), 0);
     return substr($first, 0, 80);
   }
 
   public static function convertToArcanistChanges(array $changes) {
     assert_instances_of($changes, __CLASS__);
     $direct = array();
     $result = array();
     foreach ($changes as $path) {
       $change = new ArcanistDiffChange();
       $change->setCurrentPath($path->getPath());
       $direct[] = $path->getPath();
       $change->setType($path->getChangeType());
       $file_type = $path->getFileType();
       if ($file_type == DifferentialChangeType::FILE_NORMAL) {
         $file_type = DifferentialChangeType::FILE_TEXT;
       }
       $change->setFileType($file_type);
       $change->setOldPath($path->getTargetPath());
       foreach ($path->getAwayPaths() as $away_path) {
         $change->addAwayPath($away_path);
       }
       $result[$path->getPath()] = $change;
     }
 
     return array_select_keys($result, $direct);
   }
 
   public static function convertToDifferentialChangesets(
     PhabricatorUser $user,
     array $changes) {
     assert_instances_of($changes, __CLASS__);
     $arcanist_changes = self::convertToArcanistChanges($changes);
     $diff = DifferentialDiff::newEphemeralFromRawChanges(
       $arcanist_changes);
     return $diff->getChangesets();
   }
 
   public function toDictionary() {
     $commit = $this->getCommit();
     if ($commit) {
       $commit_dict = $commit->toDictionary();
     } else {
       $commit_dict = array();
     }
     $commit_data = $this->getCommitData();
     if ($commit_data) {
       $commit_data_dict = $commit_data->toDictionary();
     } else {
       $commit_data_dict = array();
     }
     return array(
       'path' => $this->getPath(),
       'commitIdentifier' => $this->getCommitIdentifier(),
       'commit' => $commit_dict,
       'commitData' => $commit_data_dict,
       'fileType' => $this->getFileType(),
       'changeType' => $this->getChangeType(),
       'targetPath' =>  $this->getTargetPath(),
       'targetCommitIdentifier' => $this->getTargetCommitIdentifier(),
       'awayPaths' => $this->getAwayPaths(),
     );
   }
 
   public static function newFromConduit(array $dicts) {
     $results = array();
     foreach ($dicts as $dict) {
       $commit = PhabricatorRepositoryCommit::newFromDictionary($dict['commit']);
       $commit_data =
         PhabricatorRepositoryCommitData::newFromDictionary(
           $dict['commitData']);
       $results[] = id(new DiffusionPathChange())
         ->setPath($dict['path'])
         ->setCommitIdentifier($dict['commitIdentifier'])
         ->setCommit($commit)
         ->setCommitData($commit_data)
         ->setFileType($dict['fileType'])
         ->setChangeType($dict['changeType'])
         ->setTargetPath($dict['targetPath'])
         ->setTargetCommitIdentifier($dict['targetCommitIdentifier'])
         ->setAwayPaths($dict['awayPaths']);
     }
     return $results;
   }
 
 }
diff --git a/src/applications/diffusion/data/DiffusionRepositoryPath.php b/src/applications/diffusion/data/DiffusionRepositoryPath.php
index dca03d9ab..719bc2270 100644
--- a/src/applications/diffusion/data/DiffusionRepositoryPath.php
+++ b/src/applications/diffusion/data/DiffusionRepositoryPath.php
@@ -1,130 +1,130 @@
 <?php
 
-final class DiffusionRepositoryPath {
+final class DiffusionRepositoryPath extends Phobject {
 
   private $fullPath;
   private $path;
   private $hash;
   private $fileType;
   private $fileSize;
   private $externalURI;
 
   private $lastModifiedCommit;
   private $lastCommitData;
 
   public function setFullPath($full_path) {
     $this->fullPath = $full_path;
     return $this;
   }
 
   public function getFullPath() {
     return $this->fullPath;
   }
 
   public function setPath($path) {
     $this->path = $path;
     return $this;
   }
 
   public function getPath() {
     return $this->path;
   }
 
   public function setHash($hash) {
     $this->hash = $hash;
     return $this;
   }
 
   public function getHash() {
     return $this->hash;
   }
 
   public function setLastModifiedCommit(
     PhabricatorRepositoryCommit $commit) {
     $this->lastModifiedCommit = $commit;
     return $this;
   }
 
   public function getLastModifiedCommit() {
     return $this->lastModifiedCommit;
   }
 
   public function setLastCommitData(
     PhabricatorRepositoryCommitData $last_commit_data) {
     $this->lastCommitData = $last_commit_data;
     return $this;
   }
 
   public function getLastCommitData() {
     return $this->lastCommitData;
   }
 
   public function setFileType($file_type) {
     $this->fileType = $file_type;
     return $this;
   }
 
   public function getFileType() {
     return $this->fileType;
   }
 
   public function setFileSize($file_size) {
     $this->fileSize = $file_size;
     return $this;
   }
 
   public function getFileSize() {
     return $this->fileSize;
   }
 
   public function setExternalURI($external_uri) {
     $this->externalURI = $external_uri;
     return $this;
   }
 
   public function getExternalURI() {
     return $this->externalURI;
   }
 
   public function toDictionary() {
     $last_modified_commit = $this->getLastModifiedCommit();
     if ($last_modified_commit) {
       $last_modified_commit = $last_modified_commit->toDictionary();
     }
     $last_commit_data = $this->getLastCommitData();
     if ($last_commit_data) {
       $last_commit_data = $last_commit_data->toDictionary();
     }
     return array(
       'fullPath' => $this->getFullPath(),
       'path' => $this->getPath(),
       'hash' => $this->getHash(),
       'fileType' => $this->getFileType(),
       'fileSize' => $this->getFileSize(),
       'externalURI' => $this->getExternalURI(),
       'lastModifiedCommit' => $last_modified_commit,
       'lastCommitData' => $last_commit_data,
     );
   }
 
   public static function newFromDictionary(array $dict) {
     $path = id(new DiffusionRepositoryPath())
       ->setFullPath($dict['fullPath'])
       ->setPath($dict['path'])
       ->setHash($dict['hash'])
       ->setFileType($dict['fileType'])
       ->setFileSize($dict['fileSize'])
       ->setExternalURI($dict['externalURI']);
     if ($dict['lastModifiedCommit']) {
       $last_modified_commit = PhabricatorRepositoryCommit::newFromDictionary(
         $dict['lastModifiedCommit']);
       $path->setLastModifiedCommit($last_modified_commit);
     }
     if ($dict['lastCommitData']) {
       $last_commit_data = PhabricatorRepositoryCommitData::newFromDictionary(
         $dict['lastCommitData']);
       $path->setLastCommitData($last_commit_data);
     }
     return $path;
   }
 }
diff --git a/src/applications/diffusion/data/DiffusionRepositoryTag.php b/src/applications/diffusion/data/DiffusionRepositoryTag.php
index 555e08270..df8863f3c 100644
--- a/src/applications/diffusion/data/DiffusionRepositoryTag.php
+++ b/src/applications/diffusion/data/DiffusionRepositoryTag.php
@@ -1,117 +1,117 @@
 <?php
 
-final class DiffusionRepositoryTag {
+final class DiffusionRepositoryTag extends Phobject {
 
   private $author;
   private $epoch;
   private $commitIdentifier;
   private $name;
   private $description;
   private $type;
 
   private $message = false;
 
   public function setType($type) {
     $this->type = $type;
     return $this;
   }
 
   public function getType() {
     return $this->type;
   }
 
   public function setDescription($description) {
     $this->description = $description;
     return $this;
   }
 
   public function getDescription() {
     return $this->description;
   }
 
   public function setName($name) {
     $this->name = $name;
     return $this;
   }
 
   public function getName() {
     return $this->name;
   }
 
   public function setCommitIdentifier($commit_identifier) {
     $this->commitIdentifier = $commit_identifier;
     return $this;
   }
 
   public function getCommitIdentifier() {
     return $this->commitIdentifier;
   }
 
   public function setEpoch($epoch) {
     $this->epoch = $epoch;
     return $this;
   }
 
   public function getEpoch() {
     return $this->epoch;
   }
 
   public function setAuthor($author) {
     $this->author = $author;
     return $this;
   }
 
   public function getAuthor() {
     return $this->author;
   }
 
   public function attachMessage($message) {
     $this->message = $message;
     return $this;
   }
 
   public function getMessage() {
     if ($this->message === false) {
       throw new Exception(pht('Message is not attached!'));
     }
     return $this->message;
   }
 
   public function toDictionary() {
     $dict = array(
       'author' => $this->getAuthor(),
       'epoch' => $this->getEpoch(),
       'commitIdentifier' => $this->getCommitIdentifier(),
       'name' => $this->getName(),
       'description' => $this->getDescription(),
       'type' => $this->getType(),
     );
 
     if ($this->message !== false) {
       $dict['message'] = $this->message;
     }
 
     return $dict;
   }
 
   public static function newFromConduit(array $dicts) {
     $tags = array();
     foreach ($dicts as $dict) {
       $tag = id(new DiffusionRepositoryTag())
         ->setAuthor($dict['author'])
         ->setEpoch($dict['epoch'])
         ->setCommitIdentifier($dict['commitIdentifier'])
         ->setName($dict['name'])
         ->setDescription($dict['description'])
         ->setType($dict['type']);
 
       if (array_key_exists('message', $dict)) {
         $tag->attachMessage($dict['message']);
       }
 
       $tags[] = $tag;
     }
     return $tags;
   }
 
 }
diff --git a/src/applications/diffusion/protocol/DiffusionMercurialWireProtocol.php b/src/applications/diffusion/protocol/DiffusionMercurialWireProtocol.php
index 6d5257365..4f7fa6e29 100644
--- a/src/applications/diffusion/protocol/DiffusionMercurialWireProtocol.php
+++ b/src/applications/diffusion/protocol/DiffusionMercurialWireProtocol.php
@@ -1,102 +1,102 @@
 <?php
 
-final class DiffusionMercurialWireProtocol {
+final class DiffusionMercurialWireProtocol extends Phobject {
 
   public static function getCommandArgs($command) {
     // We need to enumerate all of the Mercurial wire commands because the
     // argument encoding varies based on the command. "Why?", you might ask,
     // "Why would you do this?".
 
     $commands = array(
       'batch' => array('cmds', '*'),
       'between' => array('pairs'),
       'branchmap' => array(),
       'branches' => array('nodes'),
       'capabilities' => array(),
       'changegroup' => array('roots'),
       'changegroupsubset' => array('bases heads'),
       'debugwireargs' => array('one two *'),
       'getbundle' => array('*'),
       'heads' => array(),
       'hello' => array(),
       'known' => array('nodes', '*'),
       'listkeys' => array('namespace'),
       'lookup' => array('key'),
       'pushkey' => array('namespace', 'key', 'old', 'new'),
       'stream_out' => array(''),
       'unbundle' => array('heads'),
     );
 
     if (!isset($commands[$command])) {
       throw new Exception(pht("Unknown Mercurial command '%s!", $command));
     }
 
     return $commands[$command];
   }
 
   public static function isReadOnlyCommand($command) {
     $read_only = array(
       'between' => true,
       'branchmap' => true,
       'branches' => true,
       'capabilities' => true,
       'changegroup' => true,
       'changegroupsubset' => true,
       'debugwireargs' => true,
       'getbundle' => true,
       'heads' => true,
       'hello' => true,
       'known' => true,
       'listkeys' => true,
       'lookup' => true,
       'stream_out' => true,
     );
 
     // Notably, the write commands are "pushkey" and "unbundle". The
     // "batch" command is theoretically read only, but we require explicit
     // analysis of the actual commands.
 
     return isset($read_only[$command]);
   }
 
   public static function isReadOnlyBatchCommand($cmds) {
     if (!strlen($cmds)) {
       // We expect a "batch" command to always have a "cmds" string, so err
       // on the side of caution and throw if we don't get any data here. This
       // either indicates a mangled command from the client or a programming
       // error in our code.
       throw new Exception(pht("Expected nonempty '%s' specification!", 'cmds'));
     }
 
     // For "batch" we get a "cmds" argument like:
     //
     //   heads ;known nodes=
     //
     // We need to examine the commands (here, "heads" and "known") to make sure
     // they're all read-only.
 
     // NOTE: Mercurial has some code to escape semicolons, but it does not
     // actually function for command separation. For example, these two batch
     // commands will produce completely different results (the former will run
     // the lookup; the latter will fail with a parser error):
     //
     //  lookup key=a:xb;lookup key=z* 0
     //  lookup key=a:;b;lookup key=z* 0
     //               ^
     //               |
     //               +-- Note semicolon.
     //
     // So just split unconditionally.
 
     $cmds = explode(';', $cmds);
     foreach ($cmds as $sub_cmd) {
       $name = head(explode(' ', $sub_cmd, 2));
       if (!self::isReadOnlyCommand($name)) {
         return false;
       }
     }
 
     return true;
   }
 
 }
diff --git a/src/applications/diffusion/query/DiffusionCommitQuery.php b/src/applications/diffusion/query/DiffusionCommitQuery.php
index 93efb1df1..75913ba37 100644
--- a/src/applications/diffusion/query/DiffusionCommitQuery.php
+++ b/src/applications/diffusion/query/DiffusionCommitQuery.php
@@ -1,570 +1,571 @@
 <?php
 
 final class DiffusionCommitQuery
   extends PhabricatorCursorPagedPolicyAwareQuery {
 
   private $ids;
   private $phids;
   private $authorPHIDs;
   private $defaultRepository;
   private $identifiers;
   private $repositoryIDs;
   private $repositoryPHIDs;
   private $identifierMap;
 
   private $needAuditRequests;
   private $auditIDs;
   private $auditorPHIDs;
   private $auditAwaitingUser;
   private $auditStatus;
 
   const AUDIT_STATUS_ANY       = 'audit-status-any';
   const AUDIT_STATUS_OPEN      = 'audit-status-open';
   const AUDIT_STATUS_CONCERN   = 'audit-status-concern';
   const AUDIT_STATUS_ACCEPTED  = 'audit-status-accepted';
   const AUDIT_STATUS_PARTIAL   = 'audit-status-partial';
 
   private $needCommitData;
 
   public function withIDs(array $ids) {
     $this->ids = $ids;
     return $this;
   }
 
   public function withPHIDs(array $phids) {
     $this->phids = $phids;
     return $this;
   }
 
   public function withAuthorPHIDs(array $phids) {
     $this->authorPHIDs = $phids;
     return $this;
   }
 
   /**
    * Load commits by partial or full identifiers, e.g. "rXab82393", "rX1234",
    * or "a9caf12". When an identifier matches multiple commits, they will all
    * be returned; callers should be prepared to deal with more results than
    * they queried for.
    */
   public function withIdentifiers(array $identifiers) {
     $this->identifiers = $identifiers;
     return $this;
   }
 
   /**
    * Look up commits in a specific repository. This is a shorthand for calling
    * @{method:withDefaultRepository} and @{method:withRepositoryIDs}.
    */
   public function withRepository(PhabricatorRepository $repository) {
     $this->withDefaultRepository($repository);
     $this->withRepositoryIDs(array($repository->getID()));
     return $this;
   }
 
   /**
    * Look up commits in a specific repository. Prefer
    * @{method:withRepositoryIDs}; the underyling table is keyed by ID such
    * that this method requires a separate initial query to map PHID to ID.
    */
   public function withRepositoryPHIDs(array $phids) {
     $this->repositoryPHIDs = $phids;
   }
 
   /**
    * If a default repository is provided, ambiguous commit identifiers will
    * be assumed to belong to the default repository.
    *
    * For example, "r123" appearing in a commit message in repository X is
    * likely to be unambiguously "rX123". Normally the reference would be
    * considered ambiguous, but if you provide a default repository it will
    * be correctly resolved.
    */
   public function withDefaultRepository(PhabricatorRepository $repository) {
     $this->defaultRepository = $repository;
     return $this;
   }
 
   public function withRepositoryIDs(array $repository_ids) {
     $this->repositoryIDs = $repository_ids;
     return $this;
   }
 
   public function needCommitData($need) {
     $this->needCommitData = $need;
     return $this;
   }
 
   public function needAuditRequests($need) {
     $this->needAuditRequests = $need;
     return $this;
   }
 
   /**
    * Returns true if we should join the audit table, either because we're
    * interested in the information if it's available or because matching rows
    * must always have it.
    */
   private function shouldJoinAudits() {
     return $this->auditStatus ||
            $this->rowsMustHaveAudits();
   }
 
   /**
    * Return true if we should `JOIN` (vs `LEFT JOIN`) the audit table, because
    * matching commits will always have audit rows.
    */
   private function rowsMustHaveAudits() {
     return
       $this->auditIDs ||
       $this->auditorPHIDs ||
       $this->auditAwaitingUser;
   }
 
   public function withAuditIDs(array $ids) {
     $this->auditIDs = $ids;
     return $this;
   }
 
   public function withAuditorPHIDs(array $auditor_phids) {
     $this->auditorPHIDs = $auditor_phids;
     return $this;
   }
 
   public function withAuditAwaitingUser(PhabricatorUser $user) {
     $this->auditAwaitingUser = $user;
     return $this;
   }
 
   public function withAuditStatus($status) {
     $this->auditStatus = $status;
     return $this;
   }
 
   public function getIdentifierMap() {
     if ($this->identifierMap === null) {
       throw new Exception(
         pht(
           'You must %s the query before accessing the identifier map.',
           'execute()'));
     }
     return $this->identifierMap;
   }
 
   protected function getPrimaryTableAlias() {
     return 'commit';
   }
 
   protected function willExecute() {
     if ($this->identifierMap === null) {
       $this->identifierMap = array();
     }
   }
 
   protected function loadPage() {
     $table = new PhabricatorRepositoryCommit();
     $conn_r = $table->establishConnection('r');
 
     $data = queryfx_all(
       $conn_r,
       'SELECT commit.* FROM %T commit %Q %Q %Q %Q %Q',
       $table->getTableName(),
       $this->buildJoinClause($conn_r),
       $this->buildWhereClause($conn_r),
       $this->buildGroupClause($conn_r),
       $this->buildOrderClause($conn_r),
       $this->buildLimitClause($conn_r));
 
     return $table->loadAllFromArray($data);
   }
 
   protected function willFilterPage(array $commits) {
     $repository_ids = mpull($commits, 'getRepositoryID', 'getRepositoryID');
     $repos = id(new PhabricatorRepositoryQuery())
       ->setViewer($this->getViewer())
       ->withIDs($repository_ids)
       ->execute();
 
     $min_qualified = PhabricatorRepository::MINIMUM_QUALIFIED_HASH;
     $result = array();
 
     foreach ($commits as $key => $commit) {
       $repo = idx($repos, $commit->getRepositoryID());
       if ($repo) {
         $commit->attachRepository($repo);
       } else {
+        $this->didRejectResult($commit);
         unset($commits[$key]);
         continue;
       }
 
       // Build the identifierMap
       if ($this->identifiers !== null) {
         $ids = array_fuse($this->identifiers);
         $prefixes = array(
           'r'.$commit->getRepository()->getCallsign(),
           'r'.$commit->getRepository()->getCallsign().':',
           'R'.$commit->getRepository()->getID().':',
           '', // No prefix is valid too and will only match the commitIdentifier
         );
         $suffix = $commit->getCommitIdentifier();
 
         if ($commit->getRepository()->isSVN()) {
           foreach ($prefixes as $prefix) {
             if (isset($ids[$prefix.$suffix])) {
               $result[$prefix.$suffix][] = $commit;
             }
           }
         } else {
           // This awkward construction is so we can link the commits up in O(N)
           // time instead of O(N^2).
           for ($ii = $min_qualified; $ii <= strlen($suffix); $ii++) {
             $part = substr($suffix, 0, $ii);
             foreach ($prefixes as $prefix) {
               if (isset($ids[$prefix.$part])) {
                 $result[$prefix.$part][] = $commit;
               }
             }
           }
         }
       }
     }
 
     if ($result) {
       foreach ($result as $identifier => $matching_commits) {
         if (count($matching_commits) == 1) {
           $result[$identifier] = head($matching_commits);
         } else {
           // This reference is ambiguous (it matches more than one commit) so
           // don't link it.
           unset($result[$identifier]);
         }
       }
       $this->identifierMap += $result;
     }
 
     return $commits;
   }
 
   protected function didFilterPage(array $commits) {
     if ($this->needCommitData) {
       $data = id(new PhabricatorRepositoryCommitData())->loadAllWhere(
         'commitID in (%Ld)',
         mpull($commits, 'getID'));
       $data = mpull($data, null, 'getCommitID');
       foreach ($commits as $commit) {
         $commit_data = idx($data, $commit->getID());
         if (!$commit_data) {
           $commit_data = new PhabricatorRepositoryCommitData();
         }
         $commit->attachCommitData($commit_data);
       }
     }
 
     // TODO: This should just be `needAuditRequests`, not `shouldJoinAudits()`,
     // but leave that for a future diff.
 
     if ($this->needAuditRequests || $this->shouldJoinAudits()) {
       $requests = id(new PhabricatorRepositoryAuditRequest())->loadAllWhere(
         'commitPHID IN (%Ls)',
         mpull($commits, 'getPHID'));
 
       $requests = mgroup($requests, 'getCommitPHID');
       foreach ($commits as $commit) {
         $audit_requests = idx($requests, $commit->getPHID(), array());
         $commit->attachAudits($audit_requests);
         foreach ($audit_requests as $audit_request) {
           $audit_request->attachCommit($commit);
         }
       }
     }
 
     return $commits;
   }
 
   protected function buildWhereClause(AphrontDatabaseConnection $conn_r) {
     $where = array();
 
     if ($this->repositoryPHIDs !== null) {
       $map_repositories = id (new PhabricatorRepositoryQuery())
         ->setViewer($this->getViewer())
         ->withPHIDs($this->repositoryPHIDs)
         ->execute();
 
       if (!$map_repositories) {
         throw new PhabricatorEmptyQueryException();
       }
       $repository_ids = mpull($map_repositories, 'getID');
       if ($this->repositoryIDs !== null) {
         $repository_ids = array_merge($repository_ids, $this->repositoryIDs);
       }
       $this->withRepositoryIDs($repository_ids);
     }
 
     if ($this->ids !== null) {
       $where[] = qsprintf(
         $conn_r,
         'commit.id IN (%Ld)',
         $this->ids);
     }
 
     if ($this->phids !== null) {
       $where[] = qsprintf(
         $conn_r,
         'commit.phid IN (%Ls)',
         $this->phids);
     }
 
     if ($this->repositoryIDs !== null) {
       $where[] = qsprintf(
         $conn_r,
         'commit.repositoryID IN (%Ld)',
         $this->repositoryIDs);
     }
 
     if ($this->authorPHIDs !== null) {
       $where[] = qsprintf(
         $conn_r,
         'commit.authorPHID IN (%Ls)',
         $this->authorPHIDs);
     }
 
     if ($this->identifiers !== null) {
       $min_unqualified = PhabricatorRepository::MINIMUM_UNQUALIFIED_HASH;
       $min_qualified   = PhabricatorRepository::MINIMUM_QUALIFIED_HASH;
 
       $refs = array();
       $bare = array();
       foreach ($this->identifiers as $identifier) {
         $matches = null;
         preg_match('/^(?:[rR]([A-Z]+:?|[0-9]+:))?(.*)$/',
           $identifier, $matches);
         $repo = nonempty(rtrim($matches[1], ':'), null);
         $commit_identifier = nonempty($matches[2], null);
 
         if ($repo === null) {
           if ($this->defaultRepository) {
             $repo = $this->defaultRepository->getCallsign();
           }
         }
 
         if ($repo === null) {
           if (strlen($commit_identifier) < $min_unqualified) {
             continue;
           }
           $bare[] = $commit_identifier;
         } else {
           $refs[] = array(
             'callsign' => $repo,
             'identifier' => $commit_identifier,
           );
         }
       }
 
       $sql = array();
 
       foreach ($bare as $identifier) {
         $sql[] = qsprintf(
           $conn_r,
           '(commit.commitIdentifier LIKE %> AND '.
           'LENGTH(commit.commitIdentifier) = 40)',
           $identifier);
       }
 
       if ($refs) {
         $callsigns = ipull($refs, 'callsign');
 
         $repos = id(new PhabricatorRepositoryQuery())
           ->setViewer($this->getViewer())
           ->withIdentifiers($callsigns);
         $repos->execute();
 
         $repos = $repos->getIdentifierMap();
 
         foreach ($refs as $key => $ref) {
           $repo = idx($repos, $ref['callsign']);
 
           if (!$repo) {
             continue;
           }
 
           if ($repo->isSVN()) {
             if (!ctype_digit($ref['identifier'])) {
               continue;
             }
             $sql[] = qsprintf(
               $conn_r,
               '(commit.repositoryID = %d AND commit.commitIdentifier = %s)',
               $repo->getID(),
               // NOTE: Because the 'commitIdentifier' column is a string, MySQL
               // ignores the index if we hand it an integer. Hand it a string.
               // See T3377.
               (int)$ref['identifier']);
           } else {
             if (strlen($ref['identifier']) < $min_qualified) {
               continue;
             }
             $sql[] = qsprintf(
               $conn_r,
               '(commit.repositoryID = %d AND commit.commitIdentifier LIKE %>)',
               $repo->getID(),
               $ref['identifier']);
           }
         }
       }
 
       if (!$sql) {
         // If we discarded all possible identifiers (e.g., they all referenced
         // bogus repositories or were all too short), make sure the query finds
         // nothing.
         throw new PhabricatorEmptyQueryException(
           pht('No commit identifiers.'));
       }
 
       $where[] = '('.implode(' OR ', $sql).')';
     }
 
     if ($this->auditIDs !== null) {
       $where[] = qsprintf(
         $conn_r,
         'audit.id IN (%Ld)',
         $this->auditIDs);
     }
 
     if ($this->auditorPHIDs !== null) {
       $where[] = qsprintf(
         $conn_r,
         'audit.auditorPHID IN (%Ls)',
         $this->auditorPHIDs);
     }
 
     if ($this->auditAwaitingUser) {
       $awaiting_user_phid = $this->auditAwaitingUser->getPHID();
       // Exclude package and project audits associated with commits where
       // the user is the author.
       $where[] = qsprintf(
         $conn_r,
         '(commit.authorPHID IS NULL OR commit.authorPHID != %s)
           OR (audit.auditorPHID = %s)',
         $awaiting_user_phid,
         $awaiting_user_phid);
     }
 
     $status = $this->auditStatus;
     if ($status !== null) {
       switch ($status) {
         case self::AUDIT_STATUS_PARTIAL:
           $where[] = qsprintf(
             $conn_r,
             'commit.auditStatus = %d',
             PhabricatorAuditCommitStatusConstants::PARTIALLY_AUDITED);
           break;
         case self::AUDIT_STATUS_ACCEPTED:
           $where[] = qsprintf(
             $conn_r,
             'commit.auditStatus = %d',
             PhabricatorAuditCommitStatusConstants::FULLY_AUDITED);
           break;
         case self::AUDIT_STATUS_CONCERN:
           $where[] = qsprintf(
             $conn_r,
             'audit.auditStatus = %s',
             PhabricatorAuditStatusConstants::CONCERNED);
           break;
         case self::AUDIT_STATUS_OPEN:
           $where[] = qsprintf(
             $conn_r,
             'audit.auditStatus in (%Ls)',
             PhabricatorAuditStatusConstants::getOpenStatusConstants());
           if ($this->auditAwaitingUser) {
             $where[] = qsprintf(
               $conn_r,
               'awaiting.auditStatus IS NULL OR awaiting.auditStatus != %s',
               PhabricatorAuditStatusConstants::RESIGNED);
           }
           break;
         case self::AUDIT_STATUS_ANY:
           break;
         default:
           $valid = array(
             self::AUDIT_STATUS_ANY,
             self::AUDIT_STATUS_OPEN,
             self::AUDIT_STATUS_CONCERN,
             self::AUDIT_STATUS_ACCEPTED,
             self::AUDIT_STATUS_PARTIAL,
           );
           throw new Exception(
             pht(
               "Unknown audit status '%s'! Valid statuses are: %s.",
               $status,
               implode(', ', $valid)));
       }
     }
 
     $where[] = $this->buildPagingClause($conn_r);
 
     return $this->formatWhereClause($where);
   }
 
   protected function didFilterResults(array $filtered) {
     if ($this->identifierMap) {
       foreach ($this->identifierMap as $name => $commit) {
         if (isset($filtered[$commit->getPHID()])) {
           unset($this->identifierMap[$name]);
         }
       }
     }
   }
 
   protected function buildJoinClause(AphrontDatabaseConnection $conn_r) {
     $joins = array();
     $audit_request = new PhabricatorRepositoryAuditRequest();
 
     if ($this->shouldJoinAudits()) {
       $joins[] = qsprintf(
         $conn_r,
         '%Q %T audit ON commit.phid = audit.commitPHID',
         ($this->rowsMustHaveAudits() ? 'JOIN' : 'LEFT JOIN'),
         $audit_request->getTableName());
     }
 
     if ($this->auditAwaitingUser) {
       // Join the request table on the awaiting user's requests, so we can
       // filter out package and project requests which the user has resigned
       // from.
       $joins[] = qsprintf(
         $conn_r,
         'LEFT JOIN %T awaiting ON audit.commitPHID = awaiting.commitPHID AND
         awaiting.auditorPHID = %s',
         $audit_request->getTableName(),
         $this->auditAwaitingUser->getPHID());
     }
 
     if ($joins) {
       return implode(' ', $joins);
     } else {
       return '';
     }
   }
 
   protected function buildGroupClause(AphrontDatabaseConnection $conn_r) {
     $should_group = $this->shouldJoinAudits();
 
     // TODO: Currently, the audit table is missing a unique key, so we may
     // require a GROUP BY if we perform this join. See T1768. This can be
     // removed once the table has the key.
     if ($this->auditAwaitingUser) {
       $should_group = true;
     }
 
     if ($should_group) {
       return 'GROUP BY commit.id';
     } else {
       return '';
     }
   }
 
   public function getQueryApplicationClass() {
     return 'PhabricatorDiffusionApplication';
   }
 
 }
diff --git a/src/applications/diffusion/query/DiffusionPathQuery.php b/src/applications/diffusion/query/DiffusionPathQuery.php
index f442da717..45dc978ec 100644
--- a/src/applications/diffusion/query/DiffusionPathQuery.php
+++ b/src/applications/diffusion/query/DiffusionPathQuery.php
@@ -1,43 +1,43 @@
 <?php
 
-final class DiffusionPathQuery {
+final class DiffusionPathQuery extends Phobject {
 
   private $pathIDs;
 
   public function withPathIDs(array $path_ids) {
     $this->pathIDs = $path_ids;
     return $this;
   }
 
   public function execute() {
     $conn_r = id(new PhabricatorRepository())->establishConnection('r');
 
     $where = $this->buildWhereClause($conn_r);
 
     $results = queryfx_all(
       $conn_r,
       'SELECT * FROM %T %Q',
       PhabricatorRepository::TABLE_PATH,
       $where);
 
     return ipull($results, null, 'id');
   }
 
   protected function buildWhereClause(AphrontDatabaseConnection $conn_r) {
     $where = array();
 
     if ($this->pathIDs) {
       $where[] = qsprintf(
         $conn_r,
         'id IN (%Ld)',
         $this->pathIDs);
     }
 
     if ($where) {
       return 'WHERE ('.implode(') AND (', $where).')';
     } else {
       return '';
     }
   }
 
 }
diff --git a/src/applications/diffusion/query/DiffusionRenameHistoryQuery.php b/src/applications/diffusion/query/DiffusionRenameHistoryQuery.php
index a8f561519..a2de0a1e6 100644
--- a/src/applications/diffusion/query/DiffusionRenameHistoryQuery.php
+++ b/src/applications/diffusion/query/DiffusionRenameHistoryQuery.php
@@ -1,103 +1,103 @@
 <?php
 
-final class DiffusionRenameHistoryQuery {
+final class DiffusionRenameHistoryQuery extends Phobject {
 
   private $oldCommit;
   private $wasCreated;
   private $request;
   private $viewer;
 
   public function setViewer(PhabricatorUser $viewer) {
     $this->viewer = $viewer;
     return $this;
   }
 
   public function getWasCreated() {
     return $this->wasCreated;
   }
 
   public function setRequest(DiffusionRequest $request) {
     $this->request = $request;
     return $this;
   }
 
   public function setOldCommit($old_commit) {
     $this->oldCommit = $old_commit;
     return $this;
   }
 
   public function getOldCommit() {
     return $this->oldCommit;
   }
 
   public function loadOldFilename() {
     $drequest = $this->request;
     $repository_id = $drequest->getRepository()->getID();
     $conn_r = id(new PhabricatorRepository())->establishConnection('r');
 
     $commit_id = $this->loadCommitId($this->oldCommit);
     $old_commit_sequence = $this->loadCommitSequence($commit_id);
 
     $path = '/'.$drequest->getPath();
     $commit_id = $this->loadCommitId($drequest->getCommit());
 
     do {
       $commit_sequence = $this->loadCommitSequence($commit_id);
       $change = queryfx_one(
         $conn_r,
         'SELECT pc.changeType, pc.targetCommitID, tp.path
          FROM %T p
          JOIN %T pc ON p.id = pc.pathID
          LEFT JOIN %T tp ON pc.targetPathID = tp.id
          WHERE p.pathHash = %s
          AND pc.repositoryID = %d
          AND pc.changeType IN (%d, %d)
          AND pc.commitSequence BETWEEN %d AND %d
          ORDER BY pc.commitSequence DESC
          LIMIT 1',
         PhabricatorRepository::TABLE_PATH,
         PhabricatorRepository::TABLE_PATHCHANGE,
         PhabricatorRepository::TABLE_PATH,
         md5($path),
         $repository_id,
         ArcanistDiffChangeType::TYPE_MOVE_HERE,
         ArcanistDiffChangeType::TYPE_ADD,
         $old_commit_sequence,
         $commit_sequence);
       if ($change) {
         if ($change['changeType'] == ArcanistDiffChangeType::TYPE_ADD) {
           $this->wasCreated = true;
           return $path;
         }
         $commit_id = $change['targetCommitID'];
         $path = $change['path'];
       }
     } while ($change && $path);
 
     return $path;
   }
 
   private function loadCommitId($commit_identifier) {
     $commit = id(new DiffusionCommitQuery())
       ->setViewer($this->viewer)
       ->withIdentifiers(array($commit_identifier))
       ->withRepository($this->request->getRepository())
       ->executeOne();
     return $commit->getID();
   }
 
   private function loadCommitSequence($commit_id) {
     $conn_r = id(new PhabricatorRepository())->establishConnection('r');
     $path_change = queryfx_one(
       $conn_r,
       'SELECT commitSequence
        FROM %T
        WHERE repositoryID = %d AND commitID = %d
        LIMIT 1',
       PhabricatorRepository::TABLE_PATHCHANGE,
       $this->request->getRepository()->getID(),
       $commit_id);
     return reset($path_change);
   }
 
 }
diff --git a/src/applications/diffusion/query/pathchange/DiffusionPathChangeQuery.php b/src/applications/diffusion/query/pathchange/DiffusionPathChangeQuery.php
index d2f4a2b69..8ae9b3d20 100644
--- a/src/applications/diffusion/query/pathchange/DiffusionPathChangeQuery.php
+++ b/src/applications/diffusion/query/pathchange/DiffusionPathChangeQuery.php
@@ -1,116 +1,116 @@
 <?php
 
-final class DiffusionPathChangeQuery {
+final class DiffusionPathChangeQuery extends Phobject {
 
   private $request;
   private $limit;
 
   public function setLimit($limit) {
     $this->limit = $limit;
     return $this;
   }
 
   public function getLimit() {
     return $this->limit;
   }
 
   private function __construct() {
     // <private>
   }
 
   public static function newFromDiffusionRequest(
     DiffusionRequest $request) {
     $query = new DiffusionPathChangeQuery();
     $query->request = $request;
 
     return $query;
   }
 
   protected function getRequest() {
     return $this->request;
   }
 
   public function loadChanges() {
     return $this->executeQuery();
   }
 
   protected function executeQuery() {
 
     $drequest = $this->getRequest();
     $repository = $drequest->getRepository();
 
     $commit = $drequest->loadCommit();
 
     $conn_r = $repository->establishConnection('r');
 
     $limit = '';
     if ($this->limit) {
       $limit = qsprintf(
         $conn_r,
         'LIMIT %d',
         $this->limit + 1);
     }
 
     $raw_changes = queryfx_all(
       $conn_r,
       'SELECT c.*, p.path pathName, t.path targetPathName,
           i.commitIdentifier targetCommitIdentifier
         FROM %T c
           LEFT JOIN %T p ON c.pathID = p.id
           LEFT JOIN %T t ON c.targetPathID = t.id
           LEFT JOIN %T i ON c.targetCommitID = i.id
         WHERE c.commitID = %d AND isDirect = 1 %Q',
       PhabricatorRepository::TABLE_PATHCHANGE,
       PhabricatorRepository::TABLE_PATH,
       PhabricatorRepository::TABLE_PATH,
       $commit->getTableName(),
       $commit->getID(),
       $limit);
 
     $limited = $this->limit && (count($raw_changes) > $this->limit);
     if ($limited) {
       $raw_changes = array_slice($raw_changes, 0, $this->limit);
     }
 
     $changes = array();
 
     $raw_changes = isort($raw_changes, 'pathName');
     foreach ($raw_changes as $raw_change) {
       $type = $raw_change['changeType'];
       if ($type == DifferentialChangeType::TYPE_CHILD) {
         continue;
       }
 
       $change = new DiffusionPathChange();
       $change->setPath(ltrim($raw_change['pathName'], '/'));
       $change->setChangeType($raw_change['changeType']);
       $change->setFileType($raw_change['fileType']);
       $change->setCommitIdentifier($commit->getCommitIdentifier());
 
       $change->setTargetPath(ltrim($raw_change['targetPathName'], '/'));
       $change->setTargetCommitIdentifier($raw_change['targetCommitIdentifier']);
 
       $id = $raw_change['pathID'];
       $changes[$id] = $change;
     }
 
     // Deduce the away paths by examining all the changes, if we loaded them
     // all.
 
     if (!$limited) {
       $away = array();
       foreach ($changes as $change) {
         if ($change->getTargetPath()) {
           $away[$change->getTargetPath()][] = $change->getPath();
         }
       }
       foreach ($changes as $change) {
         if (isset($away[$change->getPath()])) {
           $change->setAwayPaths($away[$change->getPath()]);
         }
       }
     }
 
     return $changes;
   }
 }
diff --git a/src/applications/diffusion/query/pathid/DiffusionPathIDQuery.php b/src/applications/diffusion/query/pathid/DiffusionPathIDQuery.php
index 62e42e579..7c9e72143 100644
--- a/src/applications/diffusion/query/pathid/DiffusionPathIDQuery.php
+++ b/src/applications/diffusion/query/pathid/DiffusionPathIDQuery.php
@@ -1,94 +1,96 @@
 <?php
 
 /**
  * @task pathutil Path Utilities
  */
-final class DiffusionPathIDQuery {
+final class DiffusionPathIDQuery extends Phobject {
+
+  private $paths = array();
 
   public function __construct(array $paths) {
     $this->paths = $paths;
   }
 
   public function loadPathIDs() {
     $repository = new PhabricatorRepository();
 
     $path_normal_map = array();
     foreach ($this->paths as $path) {
       $normal = self::normalizePath($path);
       $path_normal_map[$normal][] = $path;
     }
 
     $paths = queryfx_all(
       $repository->establishConnection('r'),
       'SELECT * FROM %T WHERE pathHash IN (%Ls)',
       PhabricatorRepository::TABLE_PATH,
       array_map('md5', array_keys($path_normal_map)));
     $paths = ipull($paths, 'id', 'path');
 
     $result = array();
 
     foreach ($path_normal_map as $normal => $originals) {
       foreach ($originals as $original) {
         $result[$original] = idx($paths, $normal);
       }
     }
 
     return $result;
   }
 
 
   /**
    * Convert a path to the canonical, absolute representation used by Diffusion.
    *
    * @param string Some repository path.
    * @return string Canonicalized Diffusion path.
    * @task pathutil
    */
   public static function normalizePath($path) {
 
     // Normalize to single slashes, e.g. "///" => "/".
     $path = preg_replace('@[/]{2,}@', '/', $path);
 
     return '/'.trim($path, '/');
   }
 
 
   /**
    * Return the canonical parent directory for a path. Note, returns "/" when
    * passed "/".
    *
    * @param string Some repository path.
    * @return string That path's canonical parent directory.
    * @task pathutil
    */
   public static function getParentPath($path) {
     $path = self::normalizePath($path);
     $path = dirname($path);
     if (phutil_is_windows() && $path == '\\') {
         $path = '/';
     }
     return $path;
   }
 
 
   /**
    * Generate a list of parents for a repository path. The path itself is
    * included.
    *
    * @param string Some repository path.
    * @return list List of canonical paths between the path and the root.
    * @task pathutil
    */
   public static function expandPathToRoot($path) {
     $path = self::normalizePath($path);
     $parents = array($path);
     $parts = explode('/', trim($path, '/'));
     while (count($parts) >= 1) {
       if (array_pop($parts)) {
         $parents[] = '/'.implode('/', $parts);
       }
     }
     return $parents;
   }
 
 }
diff --git a/src/applications/diffusion/request/DiffusionRequest.php b/src/applications/diffusion/request/DiffusionRequest.php
index 2ed8e1f88..6cf9aba8b 100644
--- a/src/applications/diffusion/request/DiffusionRequest.php
+++ b/src/applications/diffusion/request/DiffusionRequest.php
@@ -1,855 +1,855 @@
 <?php
 
 /**
  * Contains logic to parse Diffusion requests, which have a complicated URI
  * structure.
  *
  * @task new Creating Requests
  * @task uri Managing Diffusion URIs
  */
-abstract class DiffusionRequest {
+abstract class DiffusionRequest extends Phobject {
 
   protected $callsign;
   protected $path;
   protected $line;
   protected $branch;
   protected $lint;
 
   protected $symbolicCommit;
   protected $symbolicType;
   protected $stableCommit;
 
   protected $repository;
   protected $repositoryCommit;
   protected $repositoryCommitData;
 
   private $isClusterRequest = false;
   private $initFromConduit = true;
   private $user;
   private $branchObject = false;
   private $refAlternatives;
 
   abstract public function supportsBranches();
   abstract protected function isStableCommit($symbol);
 
   protected function didInitialize() {
     return null;
   }
 
 
 /* -(  Creating Requests  )-------------------------------------------------- */
 
 
   /**
    * Create a new synthetic request from a parameter dictionary. If you need
    * a @{class:DiffusionRequest} object in order to issue a DiffusionQuery, you
    * can use this method to build one.
    *
    * Parameters are:
    *
    *   - `callsign` Repository callsign. Provide this or `repository`.
    *   - `user` Viewing user. Required if `callsign` is provided.
    *   - `repository` Repository object. Provide this or `callsign`.
    *   - `branch` Optional, branch name.
    *   - `path` Optional, file path.
    *   - `commit` Optional, commit identifier.
    *   - `line` Optional, line range.
    *
    * @param   map                 See documentation.
    * @return  DiffusionRequest    New request object.
    * @task new
    */
   final public static function newFromDictionary(array $data) {
     if (isset($data['repository']) && isset($data['callsign'])) {
       throw new Exception(
         pht(
           "Specify '%s' or '%s', but not both.",
           'repository',
           'callsign'));
     } else if (!isset($data['repository']) && !isset($data['callsign'])) {
       throw new Exception(
         pht(
           "One of '%s' and '%s' is required.",
           'repository',
           'callsign'));
     } else if (isset($data['callsign']) && empty($data['user'])) {
       throw new Exception(
         pht(
           "Parameter '%s' is required if '%s' is provided.",
           'user',
           'callsign'));
     }
 
     if (isset($data['repository'])) {
       $object = self::newFromRepository($data['repository']);
     } else {
       $object = self::newFromCallsign($data['callsign'], $data['user']);
     }
 
     $object->initializeFromDictionary($data);
 
     return $object;
   }
 
 
   /**
    * Create a new request from an Aphront request dictionary. This is an
    * internal method that you generally should not call directly; instead,
    * call @{method:newFromDictionary}.
    *
    * @param   map                 Map of Aphront request data.
    * @return  DiffusionRequest    New request object.
    * @task new
    */
   final public static function newFromAphrontRequestDictionary(
     array $data,
     AphrontRequest $request) {
 
     $callsign = phutil_unescape_uri_path_component(idx($data, 'callsign'));
     $object = self::newFromCallsign($callsign, $request->getUser());
 
     $use_branches = $object->supportsBranches();
 
     if (isset($data['dblob'])) {
       $parsed = self::parseRequestBlob(idx($data, 'dblob'), $use_branches);
     } else {
       $parsed = array(
         'commit' => idx($data, 'commit'),
         'path' => idx($data, 'path'),
         'line' => idx($data, 'line'),
         'branch' => idx($data, 'branch'),
       );
     }
 
     $object->setUser($request->getUser());
     $object->initializeFromDictionary($parsed);
     $object->lint = $request->getStr('lint');
     return $object;
   }
 
 
   /**
    * Internal.
    *
    * @task new
    */
   final private function __construct() {
     // <private>
   }
 
 
   /**
    * Internal. Use @{method:newFromDictionary}, not this method.
    *
    * @param   string              Repository callsign.
    * @param   PhabricatorUser     Viewing user.
    * @return  DiffusionRequest    New request object.
    * @task new
    */
   final private static function newFromCallsign(
     $callsign,
     PhabricatorUser $viewer) {
 
     $repository = id(new PhabricatorRepositoryQuery())
       ->setViewer($viewer)
       ->withCallsigns(array($callsign))
       ->executeOne();
 
     if (!$repository) {
       throw new Exception(pht("No such repository '%s'.", $callsign));
     }
 
     return self::newFromRepository($repository);
   }
 
 
   /**
    * Internal. Use @{method:newFromDictionary}, not this method.
    *
    * @param   PhabricatorRepository   Repository object.
    * @return  DiffusionRequest        New request object.
    * @task new
    */
   final private static function newFromRepository(
     PhabricatorRepository $repository) {
 
     $map = array(
       PhabricatorRepositoryType::REPOSITORY_TYPE_GIT => 'DiffusionGitRequest',
       PhabricatorRepositoryType::REPOSITORY_TYPE_SVN => 'DiffusionSvnRequest',
       PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL =>
         'DiffusionMercurialRequest',
     );
 
     $class = idx($map, $repository->getVersionControlSystem());
 
     if (!$class) {
       throw new Exception(pht('Unknown version control system!'));
     }
 
     $object = new $class();
 
     $object->repository = $repository;
     $object->callsign   = $repository->getCallsign();
 
     return $object;
   }
 
 
   /**
    * Internal. Use @{method:newFromDictionary}, not this method.
    *
    * @param map Map of parsed data.
    * @return void
    * @task new
    */
   final private function initializeFromDictionary(array $data) {
     $this->path            = idx($data, 'path');
     $this->line            = idx($data, 'line');
     $this->initFromConduit = idx($data, 'initFromConduit', true);
 
     $this->symbolicCommit = idx($data, 'commit');
     if ($this->supportsBranches()) {
       $this->branch = idx($data, 'branch');
     }
 
     if (!$this->getUser()) {
       $user = idx($data, 'user');
       if (!$user) {
         throw new Exception(
           pht(
             'You must provide a %s in the dictionary!',
             'PhabricatorUser'));
       }
       $this->setUser($user);
     }
 
     $this->didInitialize();
   }
 
   final public function setUser(PhabricatorUser $user) {
     $this->user = $user;
     return $this;
   }
   final public function getUser() {
     return $this->user;
   }
 
   public function getRepository() {
     return $this->repository;
   }
 
   public function getCallsign() {
     return $this->callsign;
   }
 
   public function setPath($path) {
     $this->path = $path;
     return $this;
   }
 
   public function getPath() {
     return $this->path;
   }
 
   public function getLine() {
     return $this->line;
   }
 
   public function getCommit() {
 
     // TODO: Probably remove all of this.
 
     if ($this->getSymbolicCommit() !== null) {
       return $this->getSymbolicCommit();
     }
 
     return $this->getStableCommit();
   }
 
   /**
    * Get the symbolic commit associated with this request.
    *
    * A symbolic commit may be a commit hash, an abbreviated commit hash, a
    * branch name, a tag name, or an expression like "HEAD^^^". The symbolic
    * commit may also be absent.
    *
    * This method always returns the symbol present in the original request,
    * in unmodified form.
    *
    * See also @{method:getStableCommit}.
    *
    * @return string|null  Symbolic commit, if one was present in the request.
    */
   public function getSymbolicCommit() {
     return $this->symbolicCommit;
   }
 
 
   /**
    * Modify the request to move the symbolic commit elsewhere.
    *
    * @param string New symbolic commit.
    * @return this
    */
   public function updateSymbolicCommit($symbol) {
     $this->symbolicCommit = $symbol;
     $this->symbolicType = null;
     $this->stableCommit = null;
     return $this;
   }
 
 
   /**
    * Get the ref type (`commit` or `tag`) of the location associated with this
    * request.
    *
    * If a symbolic commit is present in the request, this method identifies
    * the type of the symbol. Otherwise, it identifies the type of symbol of
    * the location the request is implicitly associated with. This will probably
    * always be `commit`.
    *
    * @return string   Symbolic commit type (`commit` or `tag`).
    */
   public function getSymbolicType() {
     if ($this->symbolicType === null) {
       // As a side effect, this resolves the symbolic type.
       $this->getStableCommit();
     }
     return $this->symbolicType;
   }
 
 
   /**
    * Retrieve the stable, permanent commit name identifying the repository
    * location associated with this request.
    *
    * This returns a non-symbolic identifier for the current commit: in Git and
    * Mercurial, a 40-character SHA1; in SVN, a revision number.
    *
    * See also @{method:getSymbolicCommit}.
    *
    * @return string Stable commit name, like a git hash or SVN revision. Not
    *                a symbolic commit reference.
    */
   public function getStableCommit() {
     if (!$this->stableCommit) {
       if ($this->isStableCommit($this->symbolicCommit)) {
         $this->stableCommit = $this->symbolicCommit;
         $this->symbolicType = 'commit';
       } else {
         $this->queryStableCommit();
       }
     }
     return $this->stableCommit;
   }
 
 
   public function getBranch() {
     return $this->branch;
   }
 
   public function getLint() {
     return $this->lint;
   }
 
   protected function getArcanistBranch() {
     return $this->getBranch();
   }
 
   public function loadBranch() {
     // TODO: Get rid of this and do real Queries on real objects.
 
     if ($this->branchObject === false) {
       $this->branchObject = PhabricatorRepositoryBranch::loadBranch(
         $this->getRepository()->getID(),
         $this->getArcanistBranch());
     }
 
     return $this->branchObject;
   }
 
   public function loadCoverage() {
     // TODO: This should also die.
     $branch = $this->loadBranch();
     if (!$branch) {
       return;
     }
 
     $path = $this->getPath();
     $path_map = id(new DiffusionPathIDQuery(array($path)))->loadPathIDs();
 
     $coverage_row = queryfx_one(
       id(new PhabricatorRepository())->establishConnection('r'),
       'SELECT * FROM %T WHERE branchID = %d AND pathID = %d
         ORDER BY commitID DESC LIMIT 1',
       'repository_coverage',
       $branch->getID(),
       $path_map[$path]);
 
     if (!$coverage_row) {
       return null;
     }
 
     return idx($coverage_row, 'coverage');
   }
 
 
   public function loadCommit() {
     if (empty($this->repositoryCommit)) {
       $repository = $this->getRepository();
 
       $commit = id(new DiffusionCommitQuery())
         ->setViewer($this->getUser())
         ->withRepository($repository)
         ->withIdentifiers(array($this->getStableCommit()))
         ->executeOne();
       if ($commit) {
         $commit->attachRepository($repository);
       }
       $this->repositoryCommit = $commit;
     }
     return $this->repositoryCommit;
   }
 
   public function loadCommitData() {
     if (empty($this->repositoryCommitData)) {
       $commit = $this->loadCommit();
       $data = id(new PhabricatorRepositoryCommitData())->loadOneWhere(
         'commitID = %d',
         $commit->getID());
       if (!$data) {
         $data = new PhabricatorRepositoryCommitData();
         $data->setCommitMessage(
           pht('(This commit has not been fully parsed yet.)'));
       }
       $this->repositoryCommitData = $data;
     }
     return $this->repositoryCommitData;
   }
 
 /* -(  Managing Diffusion URIs  )-------------------------------------------- */
 
 
   /**
    * Generate a Diffusion URI using this request to provide defaults. See
    * @{method:generateDiffusionURI} for details. This method is the same, but
    * preserves the request parameters if they are not overridden.
    *
    * @param   map         See @{method:generateDiffusionURI}.
    * @return  PhutilURI   Generated URI.
    * @task uri
    */
   public function generateURI(array $params) {
     if (empty($params['stable'])) {
       $default_commit = $this->getSymbolicCommit();
     } else {
       $default_commit = $this->getStableCommit();
     }
 
     $defaults = array(
       'callsign'  => $this->getCallsign(),
       'path'      => $this->getPath(),
       'branch'    => $this->getBranch(),
       'commit'    => $default_commit,
       'lint'      => idx($params, 'lint', $this->getLint()),
     );
     foreach ($defaults as $key => $val) {
       if (!isset($params[$key])) { // Overwrite NULL.
         $params[$key] = $val;
       }
     }
     return self::generateDiffusionURI($params);
   }
 
 
   /**
    * Generate a Diffusion URI from a parameter map. Applies the correct encoding
    * and formatting to the URI. Parameters are:
    *
    *   - `action` One of `history`, `browse`, `change`, `lastmodified`,
    *     `branch`, `tags`, `branches`,  or `revision-ref`. The action specified
    *      by the URI.
    *   - `callsign` Repository callsign.
    *   - `branch` Optional if action is not `branch`, branch name.
    *   - `path` Optional, path to file.
    *   - `commit` Optional, commit identifier.
    *   - `line` Optional, line range.
    *   - `lint` Optional, lint code.
    *   - `params` Optional, query parameters.
    *
    * The function generates the specified URI and returns it.
    *
    * @param   map         See documentation.
    * @return  PhutilURI   Generated URI.
    * @task uri
    */
   public static function generateDiffusionURI(array $params) {
     $action = idx($params, 'action');
 
     $callsign = idx($params, 'callsign');
     $path     = idx($params, 'path');
     $branch   = idx($params, 'branch');
     $commit   = idx($params, 'commit');
     $line     = idx($params, 'line');
 
     if (strlen($callsign)) {
       $callsign = phutil_escape_uri_path_component($callsign).'/';
     }
 
     if (strlen($branch)) {
       $branch = phutil_escape_uri_path_component($branch).'/';
     }
 
     if (strlen($path)) {
       $path = ltrim($path, '/');
       $path = str_replace(array(';', '$'), array(';;', '$$'), $path);
       $path = phutil_escape_uri($path);
     }
 
     $path = "{$branch}{$path}";
 
     if (strlen($commit)) {
       $commit = str_replace('$', '$$', $commit);
       $commit = ';'.phutil_escape_uri($commit);
     }
 
     if (strlen($line)) {
       $line = '$'.phutil_escape_uri($line);
     }
 
     $req_callsign = false;
     $req_branch   = false;
     $req_commit   = false;
 
     switch ($action) {
       case 'history':
       case 'browse':
       case 'change':
       case 'lastmodified':
       case 'tags':
       case 'branches':
       case 'lint':
       case 'refs':
         $req_callsign = true;
         break;
       case 'branch':
         $req_callsign = true;
         $req_branch = true;
         break;
       case 'commit':
         $req_callsign = true;
         $req_commit = true;
         break;
     }
 
     if ($req_callsign && !strlen($callsign)) {
       throw new Exception(
         pht(
           "Diffusion URI action '%s' requires callsign!",
           $action));
     }
 
     if ($req_commit && !strlen($commit)) {
       throw new Exception(
         pht(
           "Diffusion URI action '%s' requires commit!",
           $action));
     }
 
     switch ($action) {
       case 'change':
       case 'history':
       case 'browse':
       case 'lastmodified':
       case 'tags':
       case 'branches':
       case 'lint':
       case 'pathtree':
       case 'refs':
         $uri = "/diffusion/{$callsign}{$action}/{$path}{$commit}{$line}";
         break;
       case 'branch':
         if (strlen($path)) {
           $uri = "/diffusion/{$callsign}repository/{$path}";
         } else {
           $uri = "/diffusion/{$callsign}";
         }
         break;
       case 'external':
         $commit = ltrim($commit, ';');
         $uri = "/diffusion/external/{$commit}/";
         break;
       case 'rendering-ref':
         // This isn't a real URI per se, it's passed as a query parameter to
         // the ajax changeset stuff but then we parse it back out as though
         // it came from a URI.
         $uri = rawurldecode("{$path}{$commit}");
         break;
       case 'commit':
         $commit = ltrim($commit, ';');
         $callsign = rtrim($callsign, '/');
         $uri = "/r{$callsign}{$commit}";
         break;
       default:
         throw new Exception(pht("Unknown Diffusion URI action '%s'!", $action));
     }
 
     if ($action == 'rendering-ref') {
       return $uri;
     }
 
     $uri = new PhutilURI($uri);
 
     if (isset($params['lint'])) {
       $params['params'] = idx($params, 'params', array()) + array(
         'lint' => $params['lint'],
       );
     }
 
     if (idx($params, 'params')) {
       $uri->setQueryParams($params['params']);
     }
 
     return $uri;
   }
 
 
   /**
    * Internal. Public only for unit tests.
    *
    * Parse the request URI into components.
    *
    * @param   string  URI blob.
    * @param   bool    True if this VCS supports branches.
    * @return  map     Parsed URI.
    *
    * @task uri
    */
   public static function parseRequestBlob($blob, $supports_branches) {
     $result = array(
       'branch'  => null,
       'path'    => null,
       'commit'  => null,
       'line'    => null,
     );
 
     $matches = null;
 
     if ($supports_branches) {
       // Consume the front part of the URI, up to the first "/". This is the
       // path-component encoded branch name.
       if (preg_match('@^([^/]+)/@', $blob, $matches)) {
         $result['branch'] = phutil_unescape_uri_path_component($matches[1]);
         $blob = substr($blob, strlen($matches[1]) + 1);
       }
     }
 
     // Consume the back part of the URI, up to the first "$". Use a negative
     // lookbehind to prevent matching '$$'. We double the '$' symbol when
     // encoding so that files with names like "money/$100" will survive.
     $pattern = '@(?:(?:^|[^$])(?:[$][$])*)[$]([\d-,]+)$@';
     if (preg_match($pattern, $blob, $matches)) {
       $result['line'] = $matches[1];
       $blob = substr($blob, 0, -(strlen($matches[1]) + 1));
     }
 
     // We've consumed the line number if it exists, so unescape "$" in the
     // rest of the string.
     $blob = str_replace('$$', '$', $blob);
 
     // Consume the commit name, stopping on ';;'. We allow any character to
     // appear in commits names, as they can sometimes be symbolic names (like
     // tag names or refs).
     if (preg_match('@(?:(?:^|[^;])(?:;;)*);([^;].*)$@', $blob, $matches)) {
       $result['commit'] = $matches[1];
       $blob = substr($blob, 0, -(strlen($matches[1]) + 1));
     }
 
     // We've consumed the commit if it exists, so unescape ";" in the rest
     // of the string.
     $blob = str_replace(';;', ';', $blob);
 
     if (strlen($blob)) {
       $result['path'] = $blob;
     }
 
     $parts = explode('/', $result['path']);
     foreach ($parts as $part) {
       // Prevent any hyjinx since we're ultimately shipping this to the
       // filesystem under a lot of workflows.
       if ($part == '..') {
         throw new Exception(pht('Invalid path URI.'));
       }
     }
 
     return $result;
   }
 
   /**
    * Check that the working copy of the repository is present and readable.
    *
    * @param   string  Path to the working copy.
    */
   protected function validateWorkingCopy($path) {
     if (!is_readable(dirname($path))) {
       $this->raisePermissionException();
     }
 
     if (!Filesystem::pathExists($path)) {
       $this->raiseCloneException();
     }
   }
 
   protected function raisePermissionException() {
     $host = php_uname('n');
     $callsign = $this->getRepository()->getCallsign();
     throw new DiffusionSetupException(
       pht(
         "The clone of this repository ('%s') on the local machine ('%s') ".
         "could not be read. Ensure that the repository is in a ".
         "location where the web server has read permissions.",
         $callsign,
         $host));
   }
 
   protected function raiseCloneException() {
     $host = php_uname('n');
     $callsign = $this->getRepository()->getCallsign();
     throw new DiffusionSetupException(
       pht(
         "The working copy for this repository ('%s') hasn't been cloned yet ".
         "on this machine ('%s'). Make sure you've started the Phabricator ".
         "daemons. If this problem persists for longer than a clone should ".
         "take, check the daemon logs (in the Daemon Console) to see if there ".
         "were errors cloning the repository. Consult the 'Diffusion User ".
         "Guide' in the documentation for help setting up repositories.",
         $callsign,
         $host));
   }
 
   private function queryStableCommit() {
     $types = array();
     if ($this->symbolicCommit) {
       $ref = $this->symbolicCommit;
     } else {
       if ($this->supportsBranches()) {
         $ref = $this->getBranch();
         $types = array(
           PhabricatorRepositoryRefCursor::TYPE_BRANCH,
         );
       } else {
         $ref = 'HEAD';
       }
     }
 
     $results = $this->resolveRefs(array($ref), $types);
 
     $matches = idx($results, $ref, array());
     if (!$matches) {
       $message = pht(
         'Ref "%s" does not exist in this repository.',
         $ref);
       throw id(new DiffusionRefNotFoundException($message))
         ->setRef($ref);
     }
 
     if (count($matches) > 1) {
       $match = $this->chooseBestRefMatch($ref, $matches);
     } else {
       $match = head($matches);
     }
 
     $this->stableCommit = $match['identifier'];
     $this->symbolicType = $match['type'];
   }
 
   public function getRefAlternatives() {
     // Make sure we've resolved the reference into a stable commit first.
     try {
       $this->getStableCommit();
     } catch (DiffusionRefNotFoundException $ex) {
       // If we have a bad reference, just return the empty set of
       // alternatives.
     }
     return $this->refAlternatives;
   }
 
   private function chooseBestRefMatch($ref, array $results) {
     // First, filter out less-desirable matches.
     $candidates = array();
     foreach ($results as $result) {
       // Exclude closed heads.
       if ($result['type'] == 'branch') {
         if (idx($result, 'closed')) {
           continue;
         }
       }
 
       $candidates[] = $result;
     }
 
     // If we filtered everything, undo the filtering.
     if (!$candidates) {
       $candidates = $results;
     }
 
     // TODO: Do a better job of selecting the best match?
     $match = head($candidates);
 
     // After choosing the best alternative, save all the alternatives so the
     // UI can show them to the user.
     if (count($candidates) > 1) {
       $this->refAlternatives = $candidates;
     }
 
     return $match;
   }
 
   private function resolveRefs(array $refs, array $types) {
     // First, try to resolve refs from fast cache sources.
     $cached_query = id(new DiffusionCachedResolveRefsQuery())
       ->setRepository($this->getRepository())
       ->withRefs($refs);
 
     if ($types) {
       $cached_query->withTypes($types);
     }
 
     $cached_results = $cached_query->execute();
 
     // Throw away all the refs we resolved. Hopefully, we'll throw away
     // everything here.
     foreach ($refs as $key => $ref) {
       if (isset($cached_results[$ref])) {
         unset($refs[$key]);
       }
     }
 
     // If we couldn't pull everything out of the cache, execute the underlying
     // VCS operation.
     if ($refs) {
       $vcs_results = DiffusionQuery::callConduitWithDiffusionRequest(
         $this->getUser(),
         $this,
         'diffusion.resolverefs',
         array(
           'types' => $types,
           'refs' => $refs,
         ));
     } else {
       $vcs_results = array();
     }
 
     return $vcs_results + $cached_results;
   }
 
   public function setIsClusterRequest($is_cluster_request) {
     $this->isClusterRequest = $is_cluster_request;
     return $this;
   }
 
   public function getIsClusterRequest() {
     return $this->isClusterRequest;
   }
 
 }
diff --git a/src/applications/diffusion/ssh/DiffusionSubversionServeSSHWorkflow.php b/src/applications/diffusion/ssh/DiffusionSubversionServeSSHWorkflow.php
index 4d085302c..257e6e9ad 100644
--- a/src/applications/diffusion/ssh/DiffusionSubversionServeSSHWorkflow.php
+++ b/src/applications/diffusion/ssh/DiffusionSubversionServeSSHWorkflow.php
@@ -1,433 +1,432 @@
 <?php
 
 /**
  * This protocol has a good spec here:
  *
  *   http://svn.apache.org/repos/asf/subversion/trunk/subversion/libsvn_ra_svn/protocol
- *
  */
 final class DiffusionSubversionServeSSHWorkflow
   extends DiffusionSubversionSSHWorkflow {
 
   private $didSeeWrite;
 
   private $inProtocol;
   private $outProtocol;
 
   private $inSeenGreeting;
 
   private $outPhaseCount = 0;
 
   private $internalBaseURI;
   private $externalBaseURI;
   private $peekBuffer;
   private $command;
 
   private function getCommand() {
     return $this->command;
   }
 
   protected function didConstruct() {
     $this->setName('svnserve');
     $this->setArguments(
       array(
         array(
           'name' => 'tunnel',
           'short' => 't',
         ),
       ));
   }
 
   protected function identifyRepository() {
     // NOTE: In SVN, we need to read the first few protocol frames before we
     // can determine which repository the user is trying to access. We're
     // going to peek at the data on the wire to identify the repository.
 
     $io_channel = $this->getIOChannel();
 
     // Before the client will send us the first protocol frame, we need to send
     // it a connection frame with server capabilities. To figure out the
     // correct frame we're going to start `svnserve`, read the frame from it,
     // send it to the client, then kill the subprocess.
 
     // TODO: This is pretty inelegant and the protocol frame will change very
     // rarely. We could cache it if we can find a reasonable way to dirty the
     // cache.
 
     $command = csprintf('svnserve -t');
     $command = PhabricatorDaemon::sudoCommandAsDaemonUser($command);
     $future = new ExecFuture('%C', $command);
     $exec_channel = new PhutilExecChannel($future);
     $exec_protocol = new DiffusionSubversionWireProtocol();
 
     while (true) {
       PhutilChannel::waitForAny(array($exec_channel));
       $exec_channel->update();
 
       $exec_message = $exec_channel->read();
       if ($exec_message !== null) {
         $messages = $exec_protocol->writeData($exec_message);
         if ($messages) {
           $message = head($messages);
           $raw = $message['raw'];
 
           // Write the greeting frame to the client.
           $io_channel->write($raw);
 
           // Kill the subprocess.
           $future->resolveKill();
           break;
         }
       }
 
       if (!$exec_channel->isOpenForReading()) {
         throw new Exception(
           pht(
             '%s subprocess exited before emitting a protocol frame.',
             'svnserve'));
       }
     }
 
     $io_protocol = new DiffusionSubversionWireProtocol();
     while (true) {
       PhutilChannel::waitForAny(array($io_channel));
       $io_channel->update();
 
       $in_message = $io_channel->read();
       if ($in_message !== null) {
         $this->peekBuffer .= $in_message;
         if (strlen($this->peekBuffer) > (1024 * 1024)) {
           throw new Exception(
             pht(
               'Client transmitted more than 1MB of data without transmitting '.
               'a recognizable protocol frame.'));
         }
 
         $messages = $io_protocol->writeData($in_message);
         if ($messages) {
           $message = head($messages);
           $struct = $message['structure'];
 
           // This is the:
           //
           //   ( version ( cap1 ... ) url ... )
           //
           // The `url` allows us to identify the repository.
 
           $uri = $struct[2]['value'];
           $path = $this->getPathFromSubversionURI($uri);
 
           return $this->loadRepositoryWithPath($path);
         }
       }
 
       if (!$io_channel->isOpenForReading()) {
         throw new Exception(
           pht(
             'Client closed connection before sending a complete protocol '.
             'frame.'));
       }
 
       // If the client has disconnected, kill the subprocess and bail.
       if (!$io_channel->isOpenForWriting()) {
         throw new Exception(
           pht(
             'Client closed connection before receiving response.'));
       }
     }
   }
 
   protected function executeRepositoryOperations() {
     $repository = $this->getRepository();
 
     $args = $this->getArgs();
     if (!$args->getArg('tunnel')) {
       throw new Exception(pht('Expected `%s`!', 'svnserve -t'));
     }
 
     if ($this->shouldProxy()) {
       $command = $this->getProxyCommand();
     } else {
       $command = csprintf(
         'svnserve -t --tunnel-user=%s',
         $this->getUser()->getUsername());
     }
 
     $command = PhabricatorDaemon::sudoCommandAsDaemonUser($command);
     $future = new ExecFuture('%C', $command);
 
     $this->inProtocol = new DiffusionSubversionWireProtocol();
     $this->outProtocol = new DiffusionSubversionWireProtocol();
 
     $this->command = id($this->newPassthruCommand())
       ->setIOChannel($this->getIOChannel())
       ->setCommandChannelFromExecFuture($future)
       ->setWillWriteCallback(array($this, 'willWriteMessageCallback'))
       ->setWillReadCallback(array($this, 'willReadMessageCallback'));
 
     $this->command->setPauseIOReads(true);
 
     $err = $this->command->execute();
 
     if (!$err && $this->didSeeWrite) {
       $this->getRepository()->writeStatusMessage(
         PhabricatorRepositoryStatusMessage::TYPE_NEEDS_UPDATE,
         PhabricatorRepositoryStatusMessage::CODE_OKAY);
     }
 
     return $err;
   }
 
   public function willWriteMessageCallback(
     PhabricatorSSHPassthruCommand $command,
     $message) {
 
     $proto = $this->inProtocol;
     $messages = $proto->writeData($message);
 
     $result = array();
     foreach ($messages as $message) {
       $message_raw = $message['raw'];
       $struct = $message['structure'];
 
       if (!$this->inSeenGreeting) {
         $this->inSeenGreeting = true;
 
         // The first message the client sends looks like:
         //
         //   ( version ( cap1 ... ) url ... )
         //
         // We want to grab the URL, load the repository, make sure it exists and
         // is accessible, and then replace it with the location of the
         // repository on disk.
 
         $uri = $struct[2]['value'];
         $struct[2]['value'] = $this->makeInternalURI($uri);
 
         $message_raw = $proto->serializeStruct($struct);
       } else if (isset($struct[0]) && $struct[0]['type'] == 'word') {
 
         if (!$proto->isReadOnlyCommand($struct)) {
           $this->didSeeWrite = true;
           $this->requireWriteAccess($struct[0]['value']);
         }
 
         // Several other commands also pass in URLs. We need to translate
         // all of these into the internal representation; this also makes sure
         // they're valid and accessible.
 
         switch ($struct[0]['value']) {
           case 'reparent':
             // ( reparent ( url ) )
             $struct[1]['value'][0]['value'] = $this->makeInternalURI(
               $struct[1]['value'][0]['value']);
             $message_raw = $proto->serializeStruct($struct);
             break;
           case 'switch':
             // ( switch ( ( rev ) target recurse url ... ) )
             $struct[1]['value'][3]['value'] = $this->makeInternalURI(
               $struct[1]['value'][3]['value']);
             $message_raw = $proto->serializeStruct($struct);
             break;
           case 'diff':
             // ( diff ( ( rev ) target recurse ignore-ancestry url ... ) )
             $struct[1]['value'][4]['value'] = $this->makeInternalURI(
               $struct[1]['value'][4]['value']);
             $message_raw = $proto->serializeStruct($struct);
             break;
           case 'add-file':
           case 'add-dir':
             // ( add-file ( path dir-token file-token [ copy-path copy-rev ] ) )
             // ( add-dir ( path parent child [ copy-path copy-rev ] ) )
             if (isset($struct[1]['value'][3]['value'][0]['value'])) {
               $copy_from = $struct[1]['value'][3]['value'][0]['value'];
               $copy_from = $this->makeInternalURI($copy_from);
               $struct[1]['value'][3]['value'][0]['value'] = $copy_from;
             }
             $message_raw = $proto->serializeStruct($struct);
             break;
         }
       }
 
       $result[] = $message_raw;
     }
 
     if (!$result) {
       return null;
     }
 
     return implode('', $result);
   }
 
   public function willReadMessageCallback(
     PhabricatorSSHPassthruCommand $command,
     $message) {
 
     $proto = $this->outProtocol;
     $messages = $proto->writeData($message);
 
     $result = array();
     foreach ($messages as $message) {
       $message_raw = $message['raw'];
       $struct = $message['structure'];
 
       if (isset($struct[0]) && ($struct[0]['type'] == 'word')) {
 
         if ($struct[0]['value'] == 'success') {
           switch ($this->outPhaseCount) {
             case 0:
               // This is the "greeting", which announces capabilities.
 
               // We already sent this when we were figuring out which
               // repository this request is for, so we aren't going to send
               // it again.
 
               // Instead, we're going to replay the client's response (which
               // we also already read).
 
               $command = $this->getCommand();
               $command->writeIORead($this->peekBuffer);
               $command->setPauseIOReads(false);
 
               $message_raw = null;
               break;
             case 1:
               // This responds to the client greeting, and announces auth.
               break;
             case 2:
               // This responds to auth, which should be trivial over SSH.
               break;
             case 3:
               // This contains the URI of the repository. We need to edit it;
               // if it does not match what the client requested it will reject
               // the response.
               $struct[1]['value'][1]['value'] = $this->makeExternalURI(
                 $struct[1]['value'][1]['value']);
               $message_raw = $proto->serializeStruct($struct);
               break;
             default:
               // We don't care about other protocol frames.
               break;
           }
 
           $this->outPhaseCount++;
         } else if ($struct[0]['value'] == 'failure') {
           // Find any error messages which include the internal URI, and
           // replace the text with the external URI.
           foreach ($struct[1]['value'] as $key => $error) {
             $code = $error['value'][0]['value'];
             $message = $error['value'][1]['value'];
 
             $message = str_replace(
               $this->internalBaseURI,
               $this->externalBaseURI,
               $message);
 
             // Derp derp derp derp derp. The structure looks like this:
             //   ( failure ( ( code message ... ) ... ) )
             $struct[1]['value'][$key]['value'][1]['value'] = $message;
           }
           $message_raw = $proto->serializeStruct($struct);
         }
 
       }
 
       if ($message_raw !== null) {
         $result[] = $message_raw;
       }
     }
 
     if (!$result) {
       return null;
     }
 
     return implode('', $result);
   }
 
   private function getPathFromSubversionURI($uri_string) {
     $uri = new PhutilURI($uri_string);
 
     $proto = $uri->getProtocol();
     if ($proto !== 'svn+ssh') {
       throw new Exception(
         pht(
           'Protocol for URI "%s" MUST be "%s".',
           $uri_string,
           'svn+ssh'));
     }
     $path = $uri->getPath();
 
     // Subversion presumably deals with this, but make sure there's nothing
     // sketchy going on with the URI.
     if (preg_match('(/\\.\\./)', $path)) {
       throw new Exception(
         pht(
           'String "%s" is invalid in path specification "%s".',
           '/../',
           $uri_string));
     }
 
     $path = $this->normalizeSVNPath($path);
 
     return $path;
   }
 
   private function makeInternalURI($uri_string) {
     $uri = new PhutilURI($uri_string);
 
     $repository = $this->getRepository();
 
     $path = $this->getPathFromSubversionURI($uri_string);
     $path = preg_replace(
       '(^/diffusion/[A-Z]+)',
       rtrim($repository->getLocalPath(), '/'),
       $path);
 
     if (preg_match('(^/diffusion/[A-Z]+/\z)', $path)) {
       $path = rtrim($path, '/');
     }
 
     // NOTE: We are intentionally NOT removing username information from the
     // URI. Subversion retains it over the course of the request and considers
     // two repositories with different username identifiers to be distinct and
     // incompatible.
 
     $uri->setPath($path);
 
     // If this is happening during the handshake, these are the base URIs for
     // the request.
     if ($this->externalBaseURI === null) {
       $pre = (string)id(clone $uri)->setPath('');
 
       $external_path = '/diffusion/'.$repository->getCallsign();
       $external_path = $this->normalizeSVNPath($external_path);
       $this->externalBaseURI = $pre.$external_path;
 
       $internal_path = rtrim($repository->getLocalPath(), '/');
       $internal_path = $this->normalizeSVNPath($internal_path);
       $this->internalBaseURI = $pre.$internal_path;
     }
 
     return (string)$uri;
   }
 
   private function makeExternalURI($uri) {
     $internal = $this->internalBaseURI;
     $external = $this->externalBaseURI;
 
     if (strncmp($uri, $internal, strlen($internal)) === 0) {
       $uri = $external.substr($uri, strlen($internal));
     }
 
     return $uri;
   }
 
   private function normalizeSVNPath($path) {
     // Subversion normalizes redundant slashes internally, so normalize them
     // here as well to make sure things match up.
     $path = preg_replace('(/+)', '/', $path);
 
     return $path;
   }
 
 }
diff --git a/src/applications/diffusion/symbol/DiffusionExternalSymbolQuery.php b/src/applications/diffusion/symbol/DiffusionExternalSymbolQuery.php
index f8ca15aab..d32f1b3f6 100644
--- a/src/applications/diffusion/symbol/DiffusionExternalSymbolQuery.php
+++ b/src/applications/diffusion/symbol/DiffusionExternalSymbolQuery.php
@@ -1,46 +1,46 @@
 <?php
 
-final class DiffusionExternalSymbolQuery {
+final class DiffusionExternalSymbolQuery extends Phobject {
   private $languages = array();
   private $types = array();
   private $names = array();
   private $contexts = array();
 
   public function withLanguages(array $languages) {
     $this->languages = $languages;
     return $this;
   }
   public function withTypes(array $types) {
     $this->types = $types;
     return $this;
   }
   public function withNames(array $names) {
     $this->names = $names;
     return $this;
   }
   public function withContexts(array $contexts) {
     $this->contexts = $contexts;
     return $this;
   }
 
 
   public function getLanguages() {
     return $this->languages;
   }
   public function getTypes() {
     return $this->types;
   }
   public function getNames() {
     return $this->names;
   }
   public function getContexts() {
     return $this->contexts;
   }
 
   public function matchesAnyLanguage(array $languages) {
     return (!$this->languages) || array_intersect($languages, $this->languages);
   }
   public function matchesAnyType(array $types) {
     return (!$this->types) || array_intersect($types, $this->types);
   }
 }
diff --git a/src/applications/diffusion/symbol/DiffusionExternalSymbolsSource.php b/src/applications/diffusion/symbol/DiffusionExternalSymbolsSource.php
index ba9ad6e5a..488971e95 100644
--- a/src/applications/diffusion/symbol/DiffusionExternalSymbolsSource.php
+++ b/src/applications/diffusion/symbol/DiffusionExternalSymbolsSource.php
@@ -1,15 +1,15 @@
 <?php
 
-abstract class DiffusionExternalSymbolsSource {
+abstract class DiffusionExternalSymbolsSource extends Phobject {
 
   /**
    * @return list of PhabricatorRepositorySymbol
    */
   abstract public function executeQuery(DiffusionExternalSymbolQuery $query);
 
   protected function buildExternalSymbol() {
     return id(new PhabricatorRepositorySymbol())
       ->setIsExternal(true)
       ->makeEphemeral();
   }
 }
diff --git a/src/applications/diviner/atom/DivinerAtom.php b/src/applications/diviner/atom/DivinerAtom.php
index adeac3680..68a0d80ca 100644
--- a/src/applications/diviner/atom/DivinerAtom.php
+++ b/src/applications/diviner/atom/DivinerAtom.php
@@ -1,435 +1,435 @@
 <?php
 
-final class DivinerAtom {
+final class DivinerAtom extends Phobject {
 
   const TYPE_ARTICLE   = 'article';
   const TYPE_CLASS     = 'class';
   const TYPE_FILE      = 'file';
   const TYPE_FUNCTION  = 'function';
   const TYPE_INTERFACE = 'interface';
   const TYPE_METHOD    = 'method';
 
   private $type;
   private $name;
   private $file;
   private $line;
   private $hash;
   private $contentRaw;
   private $length;
   private $language;
   private $docblockRaw;
   private $docblockText;
   private $docblockMeta;
   private $warnings = array();
   private $parent;
   private $parentHash;
   private $children = array();
   private $childHashes = array();
   private $context;
   private $extends = array();
   private $links = array();
   private $book;
   private $properties = array();
 
   /**
    * Returns a sorting key which imposes an unambiguous, stable order on atoms.
    */
   public function getSortKey() {
     return implode(
       "\0",
       array(
         $this->getBook(),
         $this->getType(),
         $this->getContext(),
         $this->getName(),
         $this->getFile(),
         sprintf('%08', $this->getLine()),
       ));
   }
 
   public function setBook($book) {
     $this->book = $book;
     return $this;
   }
 
   public function getBook() {
     return $this->book;
   }
 
   public function setContext($context) {
     $this->context = $context;
     return $this;
   }
 
   public function getContext() {
     return $this->context;
   }
 
   public static function getAtomSerializationVersion() {
     return 2;
   }
 
   public function addWarning($warning) {
     $this->warnings[] = $warning;
     return $this;
   }
 
   public function getWarnings() {
     return $this->warnings;
   }
 
   public function setDocblockRaw($docblock_raw) {
     $this->docblockRaw = $docblock_raw;
 
     $parser = new PhutilDocblockParser();
     list($text, $meta) = $parser->parse($docblock_raw);
     $this->docblockText = $text;
     $this->docblockMeta = $meta;
 
     return $this;
   }
 
   public function getDocblockRaw() {
     return $this->docblockRaw;
   }
 
   public function getDocblockText() {
     if ($this->docblockText === null) {
       throw new PhutilInvalidStateException('setDocblockRaw');
     }
     return $this->docblockText;
   }
 
   public function getDocblockMeta() {
     if ($this->docblockMeta === null) {
       throw new PhutilInvalidStateException('setDocblockRaw');
     }
     return $this->docblockMeta;
   }
 
   public function getDocblockMetaValue($key, $default = null) {
     $meta = $this->getDocblockMeta();
     return idx($meta, $key, $default);
   }
 
   public function setDocblockMetaValue($key, $value) {
     $meta = $this->getDocblockMeta();
     $meta[$key] = $value;
     $this->docblockMeta = $meta;
     return $this;
   }
 
   public function setType($type) {
     $this->type = $type;
     return $this;
   }
 
   public function getType() {
     return $this->type;
   }
 
   public function setName($name) {
     $this->name = $name;
     return $this;
   }
 
   public function getName() {
     return $this->name;
   }
 
   public function setFile($file) {
     $this->file = $file;
     return $this;
   }
 
   public function getFile() {
     return $this->file;
   }
 
   public function setLine($line) {
     $this->line = $line;
     return $this;
   }
 
   public function getLine() {
     return $this->line;
   }
 
   public function setContentRaw($content_raw) {
     $this->contentRaw = $content_raw;
     return $this;
   }
 
   public function getContentRaw() {
     return $this->contentRaw;
   }
 
   public function setHash($hash) {
     $this->hash = $hash;
     return $this;
   }
 
   public function addLink(DivinerAtomRef $ref) {
     $this->links[] = $ref;
     return $this;
   }
 
   public function addExtends(DivinerAtomRef $ref) {
     $this->extends[] = $ref;
     return $this;
   }
 
   public function getLinkDictionaries() {
     return mpull($this->links, 'toDictionary');
   }
 
   public function getExtendsDictionaries() {
     return mpull($this->extends, 'toDictionary');
   }
 
   public function getExtends() {
     return $this->extends;
   }
 
   public function getHash() {
     if ($this->hash) {
       return $this->hash;
     }
 
     $parts = array(
       $this->getBook(),
       $this->getType(),
       $this->getName(),
       $this->getFile(),
       $this->getLine(),
       $this->getLength(),
       $this->getLanguage(),
       $this->getContentRaw(),
       $this->getDocblockRaw(),
       $this->getProperties(),
       $this->getChildHashes(),
       mpull($this->extends, 'toHash'),
       mpull($this->links, 'toHash'),
     );
 
     $this->hash = md5(serialize($parts)).'N';
     return $this->hash;
   }
 
   public function setLength($length) {
     $this->length = $length;
     return $this;
   }
 
   public function getLength() {
     return $this->length;
   }
 
   public function setLanguage($language) {
     $this->language = $language;
     return $this;
   }
 
   public function getLanguage() {
     return $this->language;
   }
 
   public function addChildHash($child_hash) {
     $this->childHashes[] = $child_hash;
     return $this;
   }
 
   public function getChildHashes() {
     if (!$this->childHashes && $this->children) {
       $this->childHashes = mpull($this->children, 'getHash');
     }
     return $this->childHashes;
   }
 
   public function setParentHash($parent_hash) {
     if ($this->parentHash) {
       throw new Exception(pht('Atom already has a parent!'));
     }
     $this->parentHash = $parent_hash;
     return $this;
   }
 
   public function hasParent() {
     return $this->parent || $this->parentHash;
   }
 
   public function setParent(DivinerAtom $atom) {
     if ($this->parentHash) {
       throw new Exception(pht('Parent hash has already been computed!'));
     }
     $this->parent = $atom;
     return $this;
   }
 
   public function getParentHash() {
     if ($this->parent && !$this->parentHash) {
       $this->parentHash = $this->parent->getHash();
     }
     return $this->parentHash;
   }
 
   public function addChild(DivinerAtom $atom) {
     if ($this->childHashes) {
       throw new Exception(pht('Child hashes have already been computed!'));
     }
 
     $atom->setParent($this);
     $this->children[] = $atom;
     return $this;
   }
 
   public function getURI() {
     $parts = array();
     $parts[] = phutil_escape_uri_path_component($this->getType());
     if ($this->getContext()) {
       $parts[] = phutil_escape_uri_path_component($this->getContext());
     }
     $parts[] = phutil_escape_uri_path_component($this->getName());
     $parts[] = null;
     return implode('/', $parts);
   }
 
   public function toDictionary() {
     // NOTE: If you change this format, bump the format version in
     // @{method:getAtomSerializationVersion}.
     return array(
       'book'        => $this->getBook(),
       'type'        => $this->getType(),
       'name'        => $this->getName(),
       'file'        => $this->getFile(),
       'line'        => $this->getLine(),
       'hash'        => $this->getHash(),
       'uri'         => $this->getURI(),
       'length'      => $this->getLength(),
       'context'     => $this->getContext(),
       'language'    => $this->getLanguage(),
       'docblockRaw' => $this->getDocblockRaw(),
       'warnings'    => $this->getWarnings(),
       'parentHash'  => $this->getParentHash(),
       'childHashes' => $this->getChildHashes(),
       'extends'     => $this->getExtendsDictionaries(),
       'links'       => $this->getLinkDictionaries(),
       'ref'         => $this->getRef()->toDictionary(),
       'properties'  => $this->getProperties(),
     );
   }
 
   public function getRef() {
     $title = null;
     if ($this->docblockMeta) {
       $title = $this->getDocblockMetaValue('title');
     }
 
     return id(new DivinerAtomRef())
       ->setBook($this->getBook())
       ->setContext($this->getContext())
       ->setType($this->getType())
       ->setName($this->getName())
       ->setTitle($title)
       ->setGroup($this->getProperty('group'));
   }
 
   public static function newFromDictionary(array $dictionary) {
     $atom = id(new DivinerAtom())
       ->setBook(idx($dictionary, 'book'))
       ->setType(idx($dictionary, 'type'))
       ->setName(idx($dictionary, 'name'))
       ->setFile(idx($dictionary, 'file'))
       ->setLine(idx($dictionary, 'line'))
       ->setHash(idx($dictionary, 'hash'))
       ->setLength(idx($dictionary, 'length'))
       ->setContext(idx($dictionary, 'context'))
       ->setLanguage(idx($dictionary, 'language'))
       ->setParentHash(idx($dictionary, 'parentHash'))
       ->setDocblockRaw(idx($dictionary, 'docblockRaw'))
       ->setProperties(idx($dictionary, 'properties'));
 
     foreach (idx($dictionary, 'warnings', array()) as $warning) {
       $atom->addWarning($warning);
     }
 
     foreach (idx($dictionary, 'childHashes', array()) as $child) {
       $atom->addChildHash($child);
     }
 
     foreach (idx($dictionary, 'extends', array()) as $extends) {
       $atom->addExtends(DivinerAtomRef::newFromDictionary($extends));
     }
 
     return $atom;
   }
 
   public function getProperty($key, $default = null) {
     return idx($this->properties, $key, $default);
   }
 
   public function setProperty($key, $value) {
     $this->properties[$key] = $value;
   }
 
   public function getProperties() {
     return $this->properties;
   }
 
   public function setProperties(array $properties) {
     $this->properties = $properties;
     return $this;
   }
 
   public static function getThisAtomIsNotDocumentedString($type) {
     switch ($type) {
       case self::TYPE_ARTICLE:
         return pht('This article is not documented.');
       case self::TYPE_CLASS:
         return pht('This class is not documented.');
       case self::TYPE_FILE:
         return pht('This file is not documented.');
       case self::TYPE_FUNCTION:
         return pht('This function is not documented.');
       case self::TYPE_INTERFACE:
         return pht('This interface is not documented.');
       case self::TYPE_METHOD:
         return pht('This method is not documented.');
       default:
         phlog(pht("Need translation for '%s'.", $type));
         return pht('This %s is not documented.', $type);
     }
   }
 
   public static function getAllTypes() {
     return array(
       self::TYPE_ARTICLE,
       self::TYPE_CLASS,
       self::TYPE_FILE,
       self::TYPE_FUNCTION,
       self::TYPE_INTERFACE,
       self::TYPE_METHOD,
     );
   }
 
   public static function getAtomTypeNameString($type) {
     switch ($type) {
       case self::TYPE_ARTICLE:
         return pht('Article');
       case self::TYPE_CLASS:
         return pht('Class');
       case self::TYPE_FILE:
         return pht('File');
       case self::TYPE_FUNCTION:
         return pht('Function');
       case self::TYPE_INTERFACE:
         return pht('Interface');
       case self::TYPE_METHOD:
         return pht('Method');
       default:
         phlog(pht("Need translation for '%s'.", $type));
         return ucwords($type);
     }
   }
 
 }
diff --git a/src/applications/diviner/atom/DivinerAtomRef.php b/src/applications/diviner/atom/DivinerAtomRef.php
index 944429fd9..0829a5d4d 100644
--- a/src/applications/diviner/atom/DivinerAtomRef.php
+++ b/src/applications/diviner/atom/DivinerAtomRef.php
@@ -1,210 +1,210 @@
 <?php
 
-final class DivinerAtomRef {
+final class DivinerAtomRef extends Phobject {
 
   private $book;
   private $context;
   private $type;
   private $name;
   private $group;
   private $summary;
   private $index;
   private $title;
 
   public function getSortKey() {
     return implode(
       "\0",
       array(
         $this->getName(),
         $this->getType(),
         $this->getContext(),
         $this->getBook(),
         $this->getIndex(),
       ));
   }
 
   public function setIndex($index) {
     $this->index = $index;
     return $this;
   }
 
   public function getIndex() {
     return $this->index;
   }
 
   public function setSummary($summary) {
     $this->summary = $summary;
     return $this;
   }
 
   public function getSummary() {
     return $this->summary;
   }
 
   public function setName($name) {
     $normal_name = self::normalizeString($name);
     if (preg_match('/^@\d+\z/', $normal_name)) {
       throw new Exception(
         pht(
           "Atom names must not be in the form '%s'. This pattern is ".
           "reserved for disambiguating atoms with similar names.",
           '/@\d+/'));
     }
     $this->name = $normal_name;
     return $this;
   }
 
   public function getName() {
     return $this->name;
   }
 
   public function setType($type) {
     $this->type = self::normalizeString($type);
     return $this;
   }
 
   public function getType() {
     return $this->type;
   }
 
   public function setContext($context) {
     if ($context === null) {
       $this->context = $context;
     } else {
       $this->context = self::normalizeString($context);
     }
     return $this;
   }
 
   public function getContext() {
     return $this->context;
   }
 
   public function setBook($book) {
     if ($book === null) {
       $this->book = $book;
     } else {
       $this->book = self::normalizeString($book);
     }
     return $this;
   }
 
   public function getBook() {
     return $this->book;
   }
 
   public function setGroup($group) {
     $this->group = $group;
     return $this;
   }
 
   public function getGroup() {
     return $this->group;
   }
 
   public function setTitle($title) {
     $this->title = $title;
     return $this;
   }
 
   public function getTitle() {
     return $this->title;
   }
 
   public function getTitleSlug() {
     return self::normalizeTitleString($this->getTitle());
   }
 
   public function toDictionary() {
     return array(
       'book'    => $this->getBook(),
       'context' => $this->getContext(),
       'type'    => $this->getType(),
       'name'    => $this->getName(),
       'group'   => $this->getGroup(),
       'summary' => $this->getSummary(),
       'index'   => $this->getIndex(),
       'title'   => $this->getTitle(),
     );
   }
 
   public function toHash() {
     $dict = $this->toDictionary();
 
     unset($dict['group']);
     unset($dict['index']);
     unset($dict['summary']);
     unset($dict['title']);
 
     ksort($dict);
     return md5(serialize($dict)).'S';
   }
 
   public static function newFromDictionary(array $dict) {
     return id(new DivinerAtomRef())
       ->setBook(idx($dict, 'book'))
       ->setContext(idx($dict, 'context'))
       ->setType(idx($dict, 'type'))
       ->setName(idx($dict, 'name'))
       ->setGroup(idx($dict, 'group'))
       ->setSummary(idx($dict, 'summary'))
       ->setIndex(idx($dict, 'index'))
       ->setTitle(idx($dict, 'title'));
   }
 
   public static function normalizeString($str) {
     // These characters create problems on the filesystem or in URIs. Replace
     // them with non-problematic approximations (instead of simply removing
     // them) to keep the URIs fairly useful and avoid unnecessary collisions.
     // These approximations are selected based on some domain knowledge of
     // common languages: where a character is used as a delimiter, it is more
     // helpful to replace it with a "." or a ":" or similar, while it's better
     // if operator overloads read as, e.g., "operator_div".
 
     $map = array(
       // Hopefully not used anywhere by anything.
       '#' => '.',
 
       // Used in Ruby methods.
       '?' => 'Q',
 
       // Used in PHP namespaces.
       '\\' => '.',
 
       // Used in "operator +" in C++.
       '+' => 'plus',
 
       // Used in "operator %" in C++.
       '%' => 'mod',
 
       // Used in "operator /" in C++.
       '/' => 'div',
     );
     $str = str_replace(array_keys($map), array_values($map), $str);
 
     // Replace all spaces with underscores.
     $str = preg_replace('/ +/', '_', $str);
 
     // Replace control characters with "X".
     $str = preg_replace('/[\x00-\x19]/', 'X', $str);
 
     // Replace specific problematic names with alternative names.
     $alternates = array(
       '.'  => 'dot',
       '..' => 'dotdot',
       ''   => 'null',
     );
 
     return idx($alternates, $str, $str);
   }
 
   public static function normalizeTitleString($str) {
     // Remove colons from titles. This is mostly to accommodate legacy rules
     // from the old Diviner, which generated a significant number of article
     // URIs without colons present in the titles.
     $str = str_replace(':', '', $str);
     $str = self::normalizeString($str);
     return phutil_utf8_strtolower($str);
   }
 
 }
diff --git a/src/applications/diviner/atomizer/DivinerAtomizer.php b/src/applications/diviner/atomizer/DivinerAtomizer.php
index 700399e25..b86023270 100644
--- a/src/applications/diviner/atomizer/DivinerAtomizer.php
+++ b/src/applications/diviner/atomizer/DivinerAtomizer.php
@@ -1,77 +1,77 @@
 <?php
 
 /**
  * Generate @{class:DivinerAtom}s from source code.
  */
-abstract class DivinerAtomizer {
+abstract class DivinerAtomizer extends Phobject {
 
   private $book;
   private $fileName;
   private $atomContext;
 
   /**
    * If you make a significant change to an atomizer, you can bump this version
    * to drop all the old atom caches.
    */
   public static function getAtomizerVersion() {
     return 1;
   }
 
   final public function atomize($file_name, $file_data, array $context) {
     $this->fileName = $file_name;
     $this->atomContext = $context;
     $atoms = $this->executeAtomize($file_name, $file_data);
 
     // Promote the `@group` special to a property. If there's no `@group` on
     // an atom but the file it's in matches a group pattern, associate it with
     // the right group.
     foreach ($atoms as $atom) {
       $group = null;
       try {
         $group = $atom->getDocblockMetaValue('group');
       } catch (Exception $ex) {
         // There's no docblock metadata.
       }
 
       // If there's no group, but the file matches a group, use that group.
       if ($group === null && isset($context['group'])) {
         $group = $context['group'];
       }
 
       if ($group !== null) {
         $atom->setProperty('group', $group);
       }
     }
 
     return $atoms;
   }
 
   abstract protected function executeAtomize($file_name, $file_data);
 
   final public function setBook($book) {
     $this->book = $book;
     return $this;
   }
 
   final public function getBook() {
     return $this->book;
   }
 
   protected function newAtom($type) {
     return id(new DivinerAtom())
       ->setBook($this->getBook())
       ->setFile($this->fileName)
       ->setType($type);
   }
 
   protected function newRef($type, $name, $book = null, $context = null) {
     $book = coalesce($book, $this->getBook());
 
     return id(new DivinerAtomRef())
       ->setBook($book)
       ->setContext($context)
       ->setType($type)
       ->setName($name);
   }
 
 }
diff --git a/src/applications/diviner/cache/DivinerDiskCache.php b/src/applications/diviner/cache/DivinerDiskCache.php
index 56c92034e..9f8cc60c9 100644
--- a/src/applications/diviner/cache/DivinerDiskCache.php
+++ b/src/applications/diviner/cache/DivinerDiskCache.php
@@ -1,42 +1,42 @@
 <?php
 
-abstract class DivinerDiskCache {
+abstract class DivinerDiskCache extends Phobject {
 
   private $cache;
 
   public function __construct($cache_directory, $name) {
     $dir_cache = id(new PhutilDirectoryKeyValueCache())
       ->setCacheDirectory($cache_directory);
     $profiled_cache = id(new PhutilKeyValueCacheProfiler($dir_cache))
       ->setProfiler(PhutilServiceProfiler::getInstance())
       ->setName($name);
     $this->cache = $profiled_cache;
   }
 
   protected function getCache() {
     return $this->cache;
   }
 
   public function delete() {
     $this->getCache()->destroyCache();
     return $this;
   }
 
   /**
    * Convert a long-form hash key like `ccbbaaaaaaaaaaaaaaaaaaaaaaaaaaaaN` into
    * a shortened directory form, like `cc/bb/aaaaaaaaN`. In conjunction with
    * @{class:PhutilDirectoryKeyValueCache}, this gives us nice directories
    * inside .divinercache instead of a million hash files with huge names at
    * top level.
    */
   protected function getHashKey($hash) {
     return implode(
       '/',
       array(
         substr($hash, 0, 2),
         substr($hash, 2, 2),
         substr($hash, 4, 8),
       ));
   }
 
 }
diff --git a/src/applications/diviner/publisher/DivinerLivePublisher.php b/src/applications/diviner/publisher/DivinerLivePublisher.php
index 1bad2dcb8..f2dfaa0a3 100644
--- a/src/applications/diviner/publisher/DivinerLivePublisher.php
+++ b/src/applications/diviner/publisher/DivinerLivePublisher.php
@@ -1,156 +1,156 @@
 <?php
 
 final class DivinerLivePublisher extends DivinerPublisher {
 
   private $book;
 
   private function loadBook() {
     if (!$this->book) {
       $book_name = $this->getConfig('name');
 
       $book = id(new DivinerLiveBook())->loadOneWhere('name = %s', $book_name);
       if (!$book) {
         $book = id(new DivinerLiveBook())
           ->setName($book_name)
-          ->setViewPolicy(PhabricatorPolicies::POLICY_USER)
+          ->setViewPolicy(PhabricatorPolicies::getMostOpenPolicy())
           ->save();
       }
 
       $book->setConfigurationData($this->getConfigurationData())->save();
       $this->book = $book;
 
       id(new PhabricatorSearchIndexer())
         ->queueDocumentForIndexing($book->getPHID());
     }
 
     return $this->book;
   }
 
   private function loadSymbolForAtom(DivinerAtom $atom) {
     $symbol = id(new DivinerAtomQuery())
       ->setViewer(PhabricatorUser::getOmnipotentUser())
       ->withBookPHIDs(array($this->loadBook()->getPHID()))
       ->withTypes(array($atom->getType()))
       ->withNames(array($atom->getName()))
       ->withContexts(array($atom->getContext()))
       ->withIndexes(array($this->getAtomSimilarIndex($atom)))
       ->executeOne();
 
     if ($symbol) {
       return $symbol;
     }
 
     return id(new DivinerLiveSymbol())
       ->setBookPHID($this->loadBook()->getPHID())
       ->setType($atom->getType())
       ->setName($atom->getName())
       ->setContext($atom->getContext())
       ->setAtomIndex($this->getAtomSimilarIndex($atom));
   }
 
   private function loadAtomStorageForSymbol(DivinerLiveSymbol $symbol) {
     $storage = id(new DivinerLiveAtom())->loadOneWhere(
       'symbolPHID = %s',
       $symbol->getPHID());
 
     if ($storage) {
       return $storage;
     }
 
     return id(new DivinerLiveAtom())
       ->setSymbolPHID($symbol->getPHID());
   }
 
   protected function loadAllPublishedHashes() {
     $symbols = id(new DivinerAtomQuery())
       ->setViewer(PhabricatorUser::getOmnipotentUser())
       ->withBookPHIDs(array($this->loadBook()->getPHID()))
       ->withGhosts(false)
       ->execute();
 
     return mpull($symbols, 'getGraphHash');
   }
 
   protected function deleteDocumentsByHash(array $hashes) {
     $atom_table = new DivinerLiveAtom();
     $symbol_table = new DivinerLiveSymbol();
     $conn_w = $symbol_table->establishConnection('w');
 
     $strings = array();
     foreach ($hashes as $hash) {
       $strings[] = qsprintf($conn_w, '%s', $hash);
     }
 
     foreach (PhabricatorLiskDAO::chunkSQL($strings, ', ') as $chunk) {
       queryfx(
         $conn_w,
         'UPDATE %T SET graphHash = NULL, nodeHash = NULL
           WHERE graphHash IN (%Q)',
         $symbol_table->getTableName(),
         $chunk);
     }
 
     queryfx(
       $conn_w,
       'DELETE a FROM %T a LEFT JOIN %T s
         ON a.symbolPHID = s.phid
         WHERE s.graphHash IS NULL',
       $atom_table->getTableName(),
       $symbol_table->getTableName());
   }
 
   protected function createDocumentsByHash(array $hashes) {
     foreach ($hashes as $hash) {
       $atom = $this->getAtomFromGraphHash($hash);
       $ref = $atom->getRef();
 
       $symbol = $this->loadSymbolForAtom($atom);
 
       $is_documentable = $this->shouldGenerateDocumentForAtom($atom);
 
       $symbol
         ->setGraphHash($hash)
         ->setIsDocumentable((int)$is_documentable)
         ->setTitle($ref->getTitle())
         ->setGroupName($ref->getGroup())
         ->setNodeHash($atom->getHash());
 
       if ($atom->getType() !== DivinerAtom::TYPE_FILE) {
         $renderer = $this->getRenderer();
         $summary = $renderer->getAtomSummary($atom);
         $symbol->setSummary($summary);
       } else {
         $symbol->setSummary('');
       }
 
       $symbol->save();
 
       id(new PhabricatorSearchIndexer())
         ->queueDocumentForIndexing($symbol->getPHID());
 
       // TODO: We probably need a finer-grained sense of what "documentable"
       // atoms are. Neither files nor methods are currently considered
       // documentable, but for different reasons: files appear nowhere, while
       // methods just don't appear at the top level. These are probably
       // separate concepts. Since we need atoms in order to build method
       // documentation, we insert them here. This also means we insert files,
       // which are unnecessary and unused. Make sure this makes sense, but then
       // probably introduce separate "isTopLevel" and "isDocumentable" flags?
       // TODO: Yeah do that soon ^^^
 
       if ($atom->getType() !== DivinerAtom::TYPE_FILE) {
         $storage = $this->loadAtomStorageForSymbol($symbol)
           ->setAtomData($atom->toDictionary())
           ->setContent(null)
           ->save();
       }
 
     }
   }
 
   public function findAtomByRef(DivinerAtomRef $ref) {
     // TODO: Actually implement this.
     return null;
   }
 
 }
diff --git a/src/applications/diviner/publisher/DivinerPublisher.php b/src/applications/diviner/publisher/DivinerPublisher.php
index 1c0a56aaf..591897f0e 100644
--- a/src/applications/diviner/publisher/DivinerPublisher.php
+++ b/src/applications/diviner/publisher/DivinerPublisher.php
@@ -1,156 +1,156 @@
 <?php
 
-abstract class DivinerPublisher {
+abstract class DivinerPublisher extends Phobject {
 
   private $atomCache;
   private $atomGraphHashToNodeHashMap;
   private $atomMap = array();
   private $renderer;
   private $config;
   private $symbolReverseMap;
   private $dropCaches;
 
   final public function setDropCaches($drop_caches) {
     $this->dropCaches = $drop_caches;
     return $this;
   }
 
   final public function setRenderer(DivinerRenderer $renderer) {
     $renderer->setPublisher($this);
     $this->renderer = $renderer;
     return $this;
   }
 
   final public function getRenderer() {
     return $this->renderer;
   }
 
   final public function setConfig(array $config) {
     $this->config = $config;
     return $this;
   }
 
   final public function getConfig($key, $default = null) {
     return idx($this->config, $key, $default);
   }
 
   final public function getConfigurationData() {
     return $this->config;
   }
 
   final public function setAtomCache(DivinerAtomCache $cache) {
     $this->atomCache = $cache;
     $graph_map = $this->atomCache->getGraphMap();
     $this->atomGraphHashToNodeHashMap = array_flip($graph_map);
     return $this;
   }
 
   final protected function getAtomFromGraphHash($graph_hash) {
     if (empty($this->atomGraphHashToNodeHashMap[$graph_hash])) {
       throw new Exception(pht("No such atom '%s'!", $graph_hash));
     }
 
     return $this->getAtomFromNodeHash(
       $this->atomGraphHashToNodeHashMap[$graph_hash]);
   }
 
   final protected function getAtomFromNodeHash($node_hash) {
     if (empty($this->atomMap[$node_hash])) {
       $dict = $this->atomCache->getAtom($node_hash);
       $this->atomMap[$node_hash] = DivinerAtom::newFromDictionary($dict);
     }
     return $this->atomMap[$node_hash];
   }
 
   final protected function getSimilarAtoms(DivinerAtom $atom) {
     if ($this->symbolReverseMap === null) {
       $rmap = array();
       $smap = $this->atomCache->getSymbolMap();
       foreach ($smap as $nhash => $shash) {
         $rmap[$shash][$nhash] = true;
       }
       $this->symbolReverseMap = $rmap;
     }
 
     $shash = $atom->getRef()->toHash();
 
     if (empty($this->symbolReverseMap[$shash])) {
       throw new Exception(pht('Atom has no symbol map entry!'));
     }
 
     $hashes = $this->symbolReverseMap[$shash];
 
     $atoms = array();
     foreach ($hashes as $hash => $ignored) {
       $atoms[] = $this->getAtomFromNodeHash($hash);
     }
 
     $atoms = msort($atoms, 'getSortKey');
     return $atoms;
   }
 
   /**
    * If a book contains multiple definitions of some atom, like some function
    * `f()`, we assign them an arbitrary (but fairly stable) order and publish
    * them as `function/f/1/`, `function/f/2/`, etc., or similar.
    */
   final protected function getAtomSimilarIndex(DivinerAtom $atom) {
     $atoms = $this->getSimilarAtoms($atom);
     if (count($atoms) == 1) {
       return 0;
     }
 
     $index = 1;
     foreach ($atoms as $similar_atom) {
       if ($atom === $similar_atom) {
         return $index;
       }
       $index++;
     }
 
     throw new Exception(pht('Expected to find atom while disambiguating!'));
   }
 
   abstract protected function loadAllPublishedHashes();
   abstract protected function deleteDocumentsByHash(array $hashes);
   abstract protected function createDocumentsByHash(array $hashes);
   abstract public function findAtomByRef(DivinerAtomRef $ref);
 
   final public function publishAtoms(array $hashes) {
     $existing = $this->loadAllPublishedHashes();
 
     if ($this->dropCaches) {
       $deleted = $existing;
       $created = $hashes;
     } else {
       $existing_map = array_fill_keys($existing, true);
       $hashes_map = array_fill_keys($hashes, true);
 
       $deleted = array_diff_key($existing_map, $hashes_map);
       $created = array_diff_key($hashes_map, $existing_map);
 
       $deleted = array_keys($deleted);
       $created = array_keys($created);
     }
 
     echo pht('Deleting %d documents.', count($deleted))."\n";
     $this->deleteDocumentsByHash($deleted);
 
     echo pht('Creating %d documents.', count($created))."\n";
     $this->createDocumentsByHash($created);
   }
 
   final protected function shouldGenerateDocumentForAtom(DivinerAtom $atom) {
     switch ($atom->getType()) {
       case DivinerAtom::TYPE_METHOD:
       case DivinerAtom::TYPE_FILE:
         return false;
       case DivinerAtom::TYPE_ARTICLE:
       default:
         break;
     }
 
     return true;
   }
 
 }
diff --git a/src/applications/diviner/renderer/DivinerRenderer.php b/src/applications/diviner/renderer/DivinerRenderer.php
index de2141295..4fd34365a 100644
--- a/src/applications/diviner/renderer/DivinerRenderer.php
+++ b/src/applications/diviner/renderer/DivinerRenderer.php
@@ -1,40 +1,40 @@
 <?php
 
-abstract class DivinerRenderer {
+abstract class DivinerRenderer extends Phobject {
 
   private $publisher;
   private $atomStack = array();
 
   public function setPublisher($publisher) {
     $this->publisher = $publisher;
     return $this;
   }
 
   public function getPublisher() {
     return $this->publisher;
   }
 
   public function getConfig($key, $default = null) {
     return $this->getPublisher()->getConfig($key, $default);
   }
 
   protected function pushAtomStack(DivinerAtom $atom) {
     $this->atomStack[] = $atom;
     return $this;
   }
 
   protected function peekAtomStack() {
     return end($this->atomStack);
   }
 
   protected function popAtomStack() {
     array_pop($this->atomStack);
     return $this;
   }
 
   abstract public function renderAtom(DivinerAtom $atom);
   abstract public function renderAtomSummary(DivinerAtom $atom);
   abstract public function renderAtomIndex(array $refs);
   abstract public function getHrefForAtomRef(DivinerAtomRef $ref);
 
 }
diff --git a/src/applications/doorkeeper/engine/DoorkeeperFeedStoryPublisher.php b/src/applications/doorkeeper/engine/DoorkeeperFeedStoryPublisher.php
index ef98fdab4..46ca3f3ac 100644
--- a/src/applications/doorkeeper/engine/DoorkeeperFeedStoryPublisher.php
+++ b/src/applications/doorkeeper/engine/DoorkeeperFeedStoryPublisher.php
@@ -1,101 +1,101 @@
 <?php
 
 /**
  * @task config Configuration
  */
-abstract class DoorkeeperFeedStoryPublisher {
+abstract class DoorkeeperFeedStoryPublisher extends Phobject {
 
   private $feedStory;
   private $viewer;
   private $renderWithImpliedContext;
 
 
 /* -(  Configuration  )------------------------------------------------------ */
 
 
   /**
    * Render story text using contextual language to identify the object the
    * story is about, instead of the full object name. For example, without
    * contextual language a story might render like this:
    *
    *   alincoln created D123: Chop Wood for Log Cabin v2.0
    *
    * With contextual language, it will render like this instead:
    *
    *   alincoln created this revision.
    *
    * If the interface where the text will be displayed is specific to an
    * individual object (like Asana tasks that represent one review or commit
    * are), it's generally more natural to use language that assumes context.
    * If the target context may show information about several objects (like
    * JIRA issues which can have several linked revisions), it's generally
    * more useful not to assume context.
    *
    * @param bool  True to assume object context when rendering.
    * @return this
    * @task config
    */
   public function setRenderWithImpliedContext($render_with_implied_context) {
     $this->renderWithImpliedContext = $render_with_implied_context;
     return $this;
   }
 
   /**
    * Determine if rendering should assume object context. For discussion, see
    * @{method:setRenderWithImpliedContext}.
    *
    * @return bool True if rendering should assume object context is implied.
    * @task config
    */
   public function getRenderWithImpliedContext() {
     return $this->renderWithImpliedContext;
   }
 
   public function setFeedStory(PhabricatorFeedStory $feed_story) {
     $this->feedStory = $feed_story;
     return $this;
   }
 
   public function getFeedStory() {
     return $this->feedStory;
   }
 
   public function setViewer(PhabricatorUser $viewer) {
     $this->viewer = $viewer;
     return $this;
   }
 
   public function getViewer() {
     return $this->viewer;
   }
 
   abstract public function canPublishStory(
     PhabricatorFeedStory $story,
     $object);
 
   /**
    * Hook for publishers to mutate the story object, particularly by loading
    * and attaching additional data.
    */
   public function willPublishStory($object) {
     return $object;
   }
 
 
   public function getStoryText($object) {
     return $this->getFeedStory()->renderAsTextForDoorkeeper($this);
   }
 
   abstract public function isStoryAboutObjectCreation($object);
   abstract public function isStoryAboutObjectClosure($object);
   abstract public function getOwnerPHID($object);
   abstract public function getActiveUserPHIDs($object);
   abstract public function getPassiveUserPHIDs($object);
   abstract public function getCCUserPHIDs($object);
   abstract public function getObjectTitle($object);
   abstract public function getObjectURI($object);
   abstract public function getObjectDescription($object);
   abstract public function isObjectClosed($object);
   abstract public function getResponsibilityTitle($object);
 
 }
diff --git a/src/applications/drydock/application/PhabricatorDrydockApplication.php b/src/applications/drydock/application/PhabricatorDrydockApplication.php
index b2c758b56..6e7b0fcfd 100644
--- a/src/applications/drydock/application/PhabricatorDrydockApplication.php
+++ b/src/applications/drydock/application/PhabricatorDrydockApplication.php
@@ -1,85 +1,88 @@
 <?php
 
 final class PhabricatorDrydockApplication extends PhabricatorApplication {
 
   public function getBaseURI() {
     return '/drydock/';
   }
 
   public function getName() {
     return pht('Drydock');
   }
 
   public function getShortDescription() {
     return pht('Allocate Software Resources');
   }
 
   public function getFontIcon() {
     return 'fa-truck';
   }
 
   public function getTitleGlyph() {
     return "\xE2\x98\x82";
   }
 
   public function getFlavorText() {
     return pht('A nautical adventure.');
   }
 
   public function getApplicationGroup() {
     return self::GROUP_UTILITIES;
   }
 
   public function isPrototype() {
     return true;
   }
 
   public function getHelpDocumentationArticles(PhabricatorUser $viewer) {
     return array(
       array(
         'name' => pht('Drydock User Guide'),
         'href' => PhabricatorEnv::getDoclink('Drydock User Guide'),
       ),
     );
   }
 
   public function getRoutes() {
     return array(
       '/drydock/' => array(
         '' => 'DrydockConsoleController',
         'blueprint/' => array(
           '(?:query/(?P<queryKey>[^/]+)/)?' => 'DrydockBlueprintListController',
           '(?P<id>[1-9]\d*)/' => 'DrydockBlueprintViewController',
           'create/' => 'DrydockBlueprintCreateController',
           'edit/(?:(?P<id>[1-9]\d*)/)?' => 'DrydockBlueprintEditController',
         ),
         'resource/' => array(
           '(?:query/(?P<queryKey>[^/]+)/)?' => 'DrydockResourceListController',
           '(?P<id>[1-9]\d*)/' => 'DrydockResourceViewController',
           '(?P<id>[1-9]\d*)/close/' => 'DrydockResourceCloseController',
         ),
         'lease/' => array(
           '(?:query/(?P<queryKey>[^/]+)/)?' => 'DrydockLeaseListController',
           '(?P<id>[1-9]\d*)/' => 'DrydockLeaseViewController',
           '(?P<id>[1-9]\d*)/release/' => 'DrydockLeaseReleaseController',
         ),
         'log/' => array(
           '(?:query/(?P<queryKey>[^/]+)/)?' => 'DrydockLogListController',
         ),
       ),
     );
   }
 
   protected function getCustomCapabilities() {
     return array(
-      DrydockDefaultViewCapability::CAPABILITY => array(),
+      DrydockDefaultViewCapability::CAPABILITY => array(
+        'template' => DrydockBlueprintPHIDType::TYPECONST,
+      ),
       DrydockDefaultEditCapability::CAPABILITY => array(
         'default' => PhabricatorPolicies::POLICY_ADMIN,
+        'template' => DrydockBlueprintPHIDType::TYPECONST,
       ),
       DrydockCreateBlueprintsCapability::CAPABILITY => array(
         'default' => PhabricatorPolicies::POLICY_ADMIN,
       ),
     );
   }
 
 }
diff --git a/src/applications/drydock/blueprint/DrydockBlueprintImplementation.php b/src/applications/drydock/blueprint/DrydockBlueprintImplementation.php
index d98d33a5e..0abe12328 100644
--- a/src/applications/drydock/blueprint/DrydockBlueprintImplementation.php
+++ b/src/applications/drydock/blueprint/DrydockBlueprintImplementation.php
@@ -1,476 +1,476 @@
 <?php
 
 /**
  * @task lease      Lease Acquisition
  * @task resource   Resource Allocation
  * @task log        Logging
  */
-abstract class DrydockBlueprintImplementation {
+abstract class DrydockBlueprintImplementation extends Phobject {
 
   private $activeResource;
   private $activeLease;
   private $instance;
 
   abstract public function getType();
   abstract public function getInterface(
     DrydockResource $resource,
     DrydockLease $lease,
     $type);
 
   abstract public function isEnabled();
 
   abstract public function getBlueprintName();
   abstract public function getDescription();
 
   public function getBlueprintClass() {
     return get_class($this);
   }
 
   protected function loadLease($lease_id) {
     // TODO: Get rid of this?
     $query = id(new DrydockLeaseQuery())
       ->setViewer(PhabricatorUser::getOmnipotentUser())
       ->withIDs(array($lease_id))
       ->execute();
 
     $lease = idx($query, $lease_id);
 
     if (!$lease) {
       throw new Exception(pht("No such lease '%d'!", $lease_id));
     }
 
     return $lease;
   }
 
   protected function getInstance() {
     if (!$this->instance) {
       throw new Exception(
         pht('Attach the blueprint instance to the implementation.'));
     }
 
     return $this->instance;
   }
 
   public function attachInstance(DrydockBlueprint $instance) {
     $this->instance = $instance;
     return $this;
   }
 
   public function getFieldSpecifications() {
     return array();
   }
 
   public function getDetail($key, $default = null) {
     return $this->getInstance()->getDetail($key, $default);
   }
 
 
 /* -(  Lease Acquisition  )-------------------------------------------------- */
 
 
   /**
    * @task lease
    */
   final public function filterResource(
     DrydockResource $resource,
     DrydockLease $lease) {
 
     $scope = $this->pushActiveScope($resource, $lease);
 
     return $this->canAllocateLease($resource, $lease);
   }
 
 
   /**
    * Enforce basic checks on lease/resource compatibility. Allows resources to
    * reject leases if they are incompatible, even if the resource types match.
    *
    * For example, if a resource represents a 32-bit host, this method might
    * reject leases that need a 64-bit host. If a resource represents a working
    * copy of repository "X", this method might reject leases which need a
    * working copy of repository "Y". Generally, although the main types of
    * a lease and resource may match (e.g., both "host"), it may not actually be
    * possible to satisfy the lease with a specific resource.
    *
    * This method generally should not enforce limits or perform capacity
    * checks. Perform those in @{method:shouldAllocateLease} instead. It also
    * should not perform actual acquisition of the lease; perform that in
    * @{method:executeAcquireLease} instead.
    *
    * @param   DrydockResource   Candidiate resource to allocate the lease on.
    * @param   DrydockLease      Pending lease that wants to allocate here.
    * @return  bool              True if the resource and lease are compatible.
    * @task lease
    */
   abstract protected function canAllocateLease(
     DrydockResource $resource,
     DrydockLease $lease);
 
 
   /**
    * @task lease
    */
   final public function allocateLease(
     DrydockResource $resource,
     DrydockLease $lease) {
 
     $scope = $this->pushActiveScope($resource, $lease);
 
     $this->log(pht('Trying to Allocate Lease'));
 
     $lease->setStatus(DrydockLeaseStatus::STATUS_ACQUIRING);
     $lease->setResourceID($resource->getID());
     $lease->attachResource($resource);
 
     $ephemeral_lease = id(clone $lease)->makeEphemeral();
 
     $allocated = false;
     $allocation_exception = null;
 
     $resource->openTransaction();
       $resource->beginReadLocking();
         $resource->reload();
 
         // TODO: Policy stuff.
         $other_leases = id(new DrydockLease())->loadAllWhere(
           'status IN (%Ld) AND resourceID = %d',
           array(
             DrydockLeaseStatus::STATUS_ACQUIRING,
             DrydockLeaseStatus::STATUS_ACTIVE,
           ),
           $resource->getID());
 
         try {
           $allocated = $this->shouldAllocateLease(
             $resource,
             $ephemeral_lease,
             $other_leases);
         } catch (Exception $ex) {
           $allocation_exception = $ex;
         }
 
         if ($allocated) {
           $lease->save();
         }
       $resource->endReadLocking();
     if ($allocated) {
       $resource->saveTransaction();
       $this->log(pht('Allocated Lease'));
     } else {
       $resource->killTransaction();
       $this->log(pht('Failed to Allocate Lease'));
     }
 
     if ($allocation_exception) {
       $this->logException($allocation_exception);
     }
 
     return $allocated;
   }
 
 
   /**
    * Enforce lease limits on resources. Allows resources to reject leases if
    * they would become over-allocated by accepting them.
    *
    * For example, if a resource represents disk space, this method might check
    * how much space the lease is asking for (say, 200MB) and how much space is
    * left unallocated on the resource. It could grant the lease (return true)
    * if it has enough remaining space (more than 200MB), and reject the lease
    * (return false) if it does not (less than 200MB).
    *
    * A resource might also allow only exclusive leases. In this case it could
    * accept a new lease (return true) if there are no active leases, or reject
    * the new lease (return false) if there any other leases.
    *
    * A lock is held on the resource while this method executes to prevent
    * multiple processes from allocating leases on the resource simultaneously.
    * However, this means you should implement the method as cheaply as possible.
    * In particular, do not perform any actual acquisition or setup in this
    * method.
    *
    * If allocation is permitted, the lease will be moved to `ACQUIRING` status
    * and @{method:executeAcquireLease} will be called to actually perform
    * acquisition.
    *
    * General compatibility checks unrelated to resource limits and capacity are
    * better implemented in @{method:canAllocateLease}, which serves as a
    * cheap filter before lock acquisition.
    *
    * @param   DrydockResource     Candidate resource to allocate the lease on.
    * @param   DrydockLease        Pending lease that wants to allocate here.
    * @param   list<DrydockLease>  Other allocated and acquired leases on the
    *                              resource. The implementation can inspect them
    *                              to verify it can safely add the new lease.
    * @return  bool                True to allocate the lease on the resource;
    *                              false to reject it.
    * @task lease
    */
   abstract protected function shouldAllocateLease(
     DrydockResource $resource,
     DrydockLease $lease,
     array $other_leases);
 
 
   /**
    * @task lease
    */
   final public function acquireLease(
     DrydockResource $resource,
     DrydockLease $lease) {
 
     $scope = $this->pushActiveScope($resource, $lease);
 
     $this->log(pht('Acquiring Lease'));
     $lease->setStatus(DrydockLeaseStatus::STATUS_ACTIVE);
     $lease->setResourceID($resource->getID());
     $lease->attachResource($resource);
 
     $ephemeral_lease = id(clone $lease)->makeEphemeral();
 
     try {
       $this->executeAcquireLease($resource, $ephemeral_lease);
     } catch (Exception $ex) {
       $this->logException($ex);
       throw $ex;
     }
 
     $lease->setAttributes($ephemeral_lease->getAttributes());
     $lease->save();
     $this->log(pht('Acquired Lease'));
   }
 
 
   /**
    * Acquire and activate an allocated lease. Allows resources to peform setup
    * as leases are brought online.
    *
    * Following a successful call to @{method:canAllocateLease}, a lease is moved
    * to `ACQUIRING` status and this method is called after resource locks are
    * released. Nothing is locked while this method executes; the implementation
    * is free to perform expensive operations like writing files and directories,
    * executing commands, etc.
    *
    * After this method executes, the lease status is moved to `ACTIVE` and the
    * original leasee may access it.
    *
    * If acquisition fails, throw an exception.
    *
    * @param   DrydockResource   Resource to acquire a lease on.
    * @param   DrydockLease      Lease to acquire.
    * @return  void
    */
   abstract protected function executeAcquireLease(
     DrydockResource $resource,
     DrydockLease $lease);
 
 
 
   final public function releaseLease(
     DrydockResource $resource,
     DrydockLease $lease) {
     $scope = $this->pushActiveScope(null, $lease);
 
     $released = false;
 
     $lease->openTransaction();
       $lease->beginReadLocking();
         $lease->reload();
 
         if ($lease->getStatus() == DrydockLeaseStatus::STATUS_ACTIVE) {
           $lease->setStatus(DrydockLeaseStatus::STATUS_RELEASED);
           $lease->save();
           $released = true;
         }
 
       $lease->endReadLocking();
     $lease->saveTransaction();
 
     if (!$released) {
       throw new Exception(pht('Unable to release lease: lease not active!'));
     }
 
   }
 
 
 
 /* -(  Resource Allocation  )------------------------------------------------ */
 
 
   public function canAllocateMoreResources(array $pool) {
     return true;
   }
 
   abstract protected function executeAllocateResource(DrydockLease $lease);
 
 
   final public function allocateResource(DrydockLease $lease) {
     $scope = $this->pushActiveScope(null, $lease);
 
     $this->log(
       pht(
         "Blueprint '%s': Allocating Resource for '%s'",
         $this->getBlueprintClass(),
         $lease->getLeaseName()));
 
     try {
       $resource = $this->executeAllocateResource($lease);
       $this->validateAllocatedResource($resource);
     } catch (Exception $ex) {
       $this->logException($ex);
       throw $ex;
     }
 
     return $resource;
   }
 
 
 /* -(  Logging  )------------------------------------------------------------ */
 
 
   /**
    * @task log
    */
   protected function logException(Exception $ex) {
     $this->log($ex->getMessage());
   }
 
 
   /**
    * @task log
    */
   protected function log($message) {
     self::writeLog(
       $this->activeResource,
       $this->activeLease,
       $message);
   }
 
 
   /**
    * @task log
    */
   public static function writeLog(
     DrydockResource $resource = null,
     DrydockLease $lease = null,
     $message = null) {
 
     $log = id(new DrydockLog())
       ->setEpoch(time())
       ->setMessage($message);
 
     if ($resource) {
       $log->setResourceID($resource->getID());
     }
 
     if ($lease) {
       $log->setLeaseID($lease->getID());
     }
 
     $log->save();
   }
 
 
   public static function getAllBlueprintImplementations() {
     static $list = null;
 
     if ($list === null) {
       $blueprints = id(new PhutilSymbolLoader())
         ->setType('class')
         ->setAncestorClass(__CLASS__)
         ->setConcreteOnly(true)
         ->selectAndLoadSymbols();
       $list = ipull($blueprints, 'name', 'name');
       foreach ($list as $class_name => $ignored) {
         $list[$class_name] = newv($class_name, array());
       }
     }
 
     return $list;
   }
 
   public static function getAllBlueprintImplementationsForResource($type) {
     static $groups = null;
     if ($groups === null) {
       $groups = mgroup(self::getAllBlueprintImplementations(), 'getType');
     }
     return idx($groups, $type, array());
   }
 
   public static function getNamedImplementation($class) {
     return idx(self::getAllBlueprintImplementations(), $class);
   }
 
   protected function newResourceTemplate($name) {
     $resource = id(new DrydockResource())
       ->setBlueprintPHID($this->getInstance()->getPHID())
       ->setBlueprintClass($this->getBlueprintClass())
       ->setType($this->getType())
       ->setStatus(DrydockResourceStatus::STATUS_PENDING)
       ->setName($name)
       ->save();
 
     $this->activeResource = $resource;
 
     $this->log(
       pht(
         "Blueprint '%s': Created New Template",
         $this->getBlueprintClass()));
 
     return $resource;
   }
 
   /**
    * Sanity checks that the blueprint is implemented properly.
    */
   private function validateAllocatedResource($resource) {
     $blueprint = $this->getBlueprintClass();
 
     if (!($resource instanceof DrydockResource)) {
       throw new Exception(
         pht(
           "Blueprint '%s' is not properly implemented: %s must return an ".
           "object of type %s or throw, but returned something else.",
           $blueprint,
           'executeAllocateResource()',
           'DrydockResource'));
     }
 
     $current_status = $resource->getStatus();
     $req_status = DrydockResourceStatus::STATUS_OPEN;
     if ($current_status != $req_status) {
       $current_name = DrydockResourceStatus::getNameForStatus($current_status);
       $req_name = DrydockResourceStatus::getNameForStatus($req_status);
       throw new Exception(
         pht(
           "Blueprint '%s' is not properly implemented: %s must return a %s ".
           "with status '%s', but returned one with status '%s'.",
           $blueprint,
           'executeAllocateResource()',
           'DrydockResource',
           $req_name,
           $current_name));
     }
   }
 
   private function pushActiveScope(
     DrydockResource $resource = null,
     DrydockLease $lease = null) {
 
     if (($this->activeResource !== null) ||
         ($this->activeLease !== null)) {
       throw new Exception(pht('There is already an active resource or lease!'));
     }
 
     $this->activeResource = $resource;
     $this->activeLease = $lease;
 
     return new DrydockBlueprintScopeGuard($this);
   }
 
   public function popActiveScope() {
     $this->activeResource = null;
     $this->activeLease = null;
   }
 
 }
diff --git a/src/applications/drydock/blueprint/__tests__/DrydockBlueprintImplementationTestCase.php b/src/applications/drydock/blueprint/__tests__/DrydockBlueprintImplementationTestCase.php
new file mode 100644
index 000000000..b3f4a78a2
--- /dev/null
+++ b/src/applications/drydock/blueprint/__tests__/DrydockBlueprintImplementationTestCase.php
@@ -0,0 +1,10 @@
+<?php
+
+final class DrydockBlueprintImplementationTestCase extends PhabricatorTestCase {
+
+  public function testGetAllBlueprintImplementations() {
+    DrydockBlueprintImplementation::getAllBlueprintImplementations();
+    $this->assertTrue(true);
+  }
+
+}
diff --git a/src/applications/drydock/constants/DrydockConstants.php b/src/applications/drydock/constants/DrydockConstants.php
index 0905d6723..48d06329b 100644
--- a/src/applications/drydock/constants/DrydockConstants.php
+++ b/src/applications/drydock/constants/DrydockConstants.php
@@ -1,3 +1,3 @@
 <?php
 
-abstract class DrydockConstants {}
+abstract class DrydockConstants extends Phobject {}
diff --git a/src/applications/drydock/interface/DrydockInterface.php b/src/applications/drydock/interface/DrydockInterface.php
index dcf8eb5d2..631481f7e 100644
--- a/src/applications/drydock/interface/DrydockInterface.php
+++ b/src/applications/drydock/interface/DrydockInterface.php
@@ -1,18 +1,18 @@
 <?php
 
-abstract class DrydockInterface {
+abstract class DrydockInterface extends Phobject {
 
   private $config;
 
   abstract public function getInterfaceType();
 
   final public function setConfiguration(array $config) {
     $this->config = $config;
     return $this;
   }
 
   final protected function getConfig($key, $default = null) {
     return idx($this->config, $key, $default);
   }
 
 }
diff --git a/src/applications/drydock/interface/command/DrydockSSHCommandInterface.php b/src/applications/drydock/interface/command/DrydockSSHCommandInterface.php
index e691e0ecf..362e60e3c 100644
--- a/src/applications/drydock/interface/command/DrydockSSHCommandInterface.php
+++ b/src/applications/drydock/interface/command/DrydockSSHCommandInterface.php
@@ -1,107 +1,107 @@
 <?php
 
 final class DrydockSSHCommandInterface extends DrydockCommandInterface {
 
   private $passphraseSSHKey;
   private $connectTimeout;
 
   private function openCredentialsIfNotOpen() {
     if ($this->passphraseSSHKey !== null) {
       return;
     }
 
     $credential = id(new PassphraseCredentialQuery())
       ->setViewer(PhabricatorUser::getOmnipotentUser())
       ->withIDs(array($this->getConfig('credential')))
       ->needSecrets(true)
       ->executeOne();
 
     if ($credential === null) {
       throw new Exception(
         pht(
           'There is no credential with ID %d.',
           $this->getConfig('credential')));
     }
 
     if ($credential->getProvidesType() !==
-      PassphraseCredentialTypeSSHPrivateKey::PROVIDES_TYPE) {
+      PassphraseSSHPrivateKeyCredentialType::PROVIDES_TYPE) {
       throw new Exception(pht('Only private key credentials are supported.'));
     }
 
     $this->passphraseSSHKey = PassphraseSSHKey::loadFromPHID(
       $credential->getPHID(),
       PhabricatorUser::getOmnipotentUser());
   }
 
   public function setConnectTimeout($timeout) {
     $this->connectTimeout = $timeout;
     return $this;
   }
 
   public function getExecFuture($command) {
     $this->openCredentialsIfNotOpen();
 
     $argv = func_get_args();
 
     if ($this->getConfig('platform') === 'windows') {
       // Handle Windows by executing the command under PowerShell.
       $command = id(new PhutilCommandString($argv))
         ->setEscapingMode(PhutilCommandString::MODE_POWERSHELL);
 
       $change_directory = '';
       if ($this->getWorkingDirectory() !== null) {
         $change_directory .= 'cd '.$this->getWorkingDirectory();
       }
 
       $script = <<<EOF
 $change_directory
 $command
 if (\$LastExitCode -ne 0) {
   exit \$LastExitCode
 }
 EOF;
 
       // When Microsoft says "Unicode" they don't mean UTF-8.
       $script = mb_convert_encoding($script, 'UTF-16LE');
 
       $script = base64_encode($script);
 
       $powershell =
         'C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe';
       $powershell .=
         ' -ExecutionPolicy Bypass'.
         ' -NonInteractive'.
         ' -InputFormat Text'.
         ' -OutputFormat Text'.
         ' -EncodedCommand '.$script;
 
       $full_command = $powershell;
     } else {
       // Handle UNIX by executing under the native shell.
       $argv = $this->applyWorkingDirectoryToArgv($argv);
 
       $full_command = call_user_func_array('csprintf', $argv);
     }
 
     $command_timeout = '';
     if ($this->connectTimeout !== null) {
       $command_timeout = csprintf(
         '-o %s',
         'ConnectTimeout='.$this->connectTimeout);
     }
 
     return new ExecFuture(
       'ssh '.
       '-o LogLevel=quiet '.
       '-o StrictHostKeyChecking=no '.
       '-o UserKnownHostsFile=/dev/null '.
       '-o BatchMode=yes '.
       '%C -p %s -i %P %P@%s -- %s',
       $command_timeout,
       $this->getConfig('port'),
       $this->passphraseSSHKey->getKeyfileEnvelope(),
       $this->passphraseSSHKey->getUsernameEnvelope(),
       $this->getConfig('host'),
       $full_command);
   }
 }
diff --git a/src/applications/drydock/interface/filesystem/DrydockSFTPFilesystemInterface.php b/src/applications/drydock/interface/filesystem/DrydockSFTPFilesystemInterface.php
index fe7d2d1cd..41b126483 100644
--- a/src/applications/drydock/interface/filesystem/DrydockSFTPFilesystemInterface.php
+++ b/src/applications/drydock/interface/filesystem/DrydockSFTPFilesystemInterface.php
@@ -1,65 +1,65 @@
 <?php
 
 final class DrydockSFTPFilesystemInterface extends DrydockFilesystemInterface {
 
   private $passphraseSSHKey;
 
   private function openCredentialsIfNotOpen() {
     if ($this->passphraseSSHKey !== null) {
       return;
     }
 
     $credential = id(new PassphraseCredentialQuery())
       ->setViewer(PhabricatorUser::getOmnipotentUser())
       ->withIDs(array($this->getConfig('credential')))
       ->needSecrets(true)
       ->executeOne();
 
     if ($credential->getProvidesType() !==
-      PassphraseCredentialTypeSSHPrivateKey::PROVIDES_TYPE) {
+      PassphraseSSHPrivateKeyCredentialType::PROVIDES_TYPE) {
       throw new Exception(pht('Only private key credentials are supported.'));
     }
 
     $this->passphraseSSHKey = PassphraseSSHKey::loadFromPHID(
       $credential->getPHID(),
       PhabricatorUser::getOmnipotentUser());
   }
 
   private function getExecFuture($path) {
     $this->openCredentialsIfNotOpen();
 
     return new ExecFuture(
       'sftp -o "StrictHostKeyChecking no" -P %s -i %P %P@%s',
       $this->getConfig('port'),
       $this->passphraseSSHKey->getKeyfileEnvelope(),
       $this->passphraseSSHKey->getUsernameEnvelope(),
       $this->getConfig('host'));
   }
 
   public function readFile($path) {
     $target = new TempFile();
     $future = $this->getExecFuture($path);
     $future->write(csprintf('get %s %s', $path, $target));
     $future->resolvex();
     return Filesystem::readFile($target);
   }
 
   public function saveFile($path, $name) {
     $data = $this->readFile($path);
     $file = PhabricatorFile::newFromFileData(
       $data,
       array('name' => $name));
     $file->setName($name);
     $file->save();
     return $file;
   }
 
   public function writeFile($path, $data) {
     $source = new TempFile();
     Filesystem::writeFile($source, $data);
     $future = $this->getExecFuture($path);
     $future->write(csprintf('put %s %s', $source, $path));
     $future->resolvex();
   }
 
 }
diff --git a/src/applications/drydock/util/DrydockBlueprintScopeGuard.php b/src/applications/drydock/util/DrydockBlueprintScopeGuard.php
index ab8268ba1..343428683 100644
--- a/src/applications/drydock/util/DrydockBlueprintScopeGuard.php
+++ b/src/applications/drydock/util/DrydockBlueprintScopeGuard.php
@@ -1,13 +1,15 @@
 <?php
 
-final class DrydockBlueprintScopeGuard {
+final class DrydockBlueprintScopeGuard extends Phobject {
+
+  private $blueprint;
 
   public function __construct(DrydockBlueprintImplementation $blueprint) {
     $this->blueprint = $blueprint;
   }
 
   public function __destruct() {
     $this->blueprint->popActiveScope();
   }
 
 }
diff --git a/src/applications/fact/engine/PhabricatorFactEngine.php b/src/applications/fact/engine/PhabricatorFactEngine.php
index 99cee6a72..734b3296c 100644
--- a/src/applications/fact/engine/PhabricatorFactEngine.php
+++ b/src/applications/fact/engine/PhabricatorFactEngine.php
@@ -1,39 +1,39 @@
 <?php
 
-abstract class PhabricatorFactEngine {
+abstract class PhabricatorFactEngine extends Phobject {
 
   final public static function loadAllEngines() {
     $classes = id(new PhutilSymbolLoader())
       ->setAncestorClass(__CLASS__)
       ->setConcreteOnly(true)
       ->selectAndLoadSymbols();
 
     $objects = array();
     foreach ($classes as $class) {
       $objects[] = newv($class['name'], array());
     }
 
     return $objects;
   }
 
   public function getFactSpecs(array $fact_types) {
     return array();
   }
 
   public function shouldComputeRawFactsForObject(PhabricatorLiskDAO $object) {
     return false;
   }
 
   public function computeRawFactsForObject(PhabricatorLiskDAO $object) {
     return array();
   }
 
   public function shouldComputeAggregateFacts() {
     return false;
   }
 
   public function computeAggregateFacts() {
     return array();
   }
 
 }
diff --git a/src/applications/fact/engine/__tests__/PhabricatorFactEngineTestCase.php b/src/applications/fact/engine/__tests__/PhabricatorFactEngineTestCase.php
new file mode 100644
index 000000000..fd665d237
--- /dev/null
+++ b/src/applications/fact/engine/__tests__/PhabricatorFactEngineTestCase.php
@@ -0,0 +1,10 @@
+<?php
+
+final class PhabricatorFactEngineTestCase extends PhabricatorTestCase {
+
+  public function testLoadAllEngines() {
+    PhabricatorFactEngine::loadAllEngines();
+    $this->assertTrue(true);
+  }
+
+}
diff --git a/src/applications/fact/spec/PhabricatorFactSpec.php b/src/applications/fact/spec/PhabricatorFactSpec.php
index a9646b246..47fcc01d8 100644
--- a/src/applications/fact/spec/PhabricatorFactSpec.php
+++ b/src/applications/fact/spec/PhabricatorFactSpec.php
@@ -1,53 +1,53 @@
 <?php
 
-abstract class PhabricatorFactSpec {
+abstract class PhabricatorFactSpec extends Phobject {
 
   const UNIT_COUNT = 'unit-count';
   const UNIT_EPOCH = 'unit-epoch';
 
   public static function newSpecsForFactTypes(
     array $engines,
     array $fact_types) {
     assert_instances_of($engines, 'PhabricatorFactEngine');
 
     $map = array();
     foreach ($engines as $engine) {
       $specs = $engine->getFactSpecs($fact_types);
       $specs = mpull($specs, null, 'getType');
       $map += $specs;
     }
 
     foreach ($fact_types as $type) {
       if (empty($map[$type])) {
         $map[$type] = new PhabricatorFactSimpleSpec($type);
       }
     }
 
     return $map;
   }
 
   abstract public function getType();
 
   public function getUnit() {
     return null;
   }
 
   public function getName() {
     return pht(
       'Fact (%s)',
       $this->getType());
   }
 
   public function formatValueForDisplay(PhabricatorUser $user, $value) {
     $unit = $this->getUnit();
     switch ($unit) {
       case self::UNIT_COUNT:
         return number_format($value);
       case self::UNIT_EPOCH:
         return phabricator_datetime($value, $user);
       default:
         return $value;
     }
   }
 
 }
diff --git a/src/applications/feed/PhabricatorFeedStoryPublisher.php b/src/applications/feed/PhabricatorFeedStoryPublisher.php
index 4e91be852..5a3ec6ea8 100644
--- a/src/applications/feed/PhabricatorFeedStoryPublisher.php
+++ b/src/applications/feed/PhabricatorFeedStoryPublisher.php
@@ -1,299 +1,299 @@
 <?php
 
-final class PhabricatorFeedStoryPublisher {
+final class PhabricatorFeedStoryPublisher extends Phobject {
 
   private $relatedPHIDs;
   private $storyType;
   private $storyData;
   private $storyTime;
   private $storyAuthorPHID;
   private $primaryObjectPHID;
   private $subscribedPHIDs = array();
   private $mailRecipientPHIDs = array();
   private $notifyAuthor;
   private $mailTags = array();
 
   public function setMailTags(array $mail_tags) {
     $this->mailTags = $mail_tags;
     return $this;
   }
 
   public function getMailTags() {
     return $this->mailTags;
   }
 
   public function setNotifyAuthor($notify_author) {
     $this->notifyAuthor = $notify_author;
     return $this;
   }
 
   public function getNotifyAuthor() {
     return $this->notifyAuthor;
   }
 
   public function setRelatedPHIDs(array $phids) {
     $this->relatedPHIDs = $phids;
     return $this;
   }
 
   public function setSubscribedPHIDs(array $phids) {
     $this->subscribedPHIDs = $phids;
     return $this;
   }
 
   public function setPrimaryObjectPHID($phid) {
     $this->primaryObjectPHID = $phid;
     return $this;
   }
 
   public function setStoryType($story_type) {
     $this->storyType = $story_type;
     return $this;
   }
 
   public function setStoryData(array $data) {
     $this->storyData = $data;
     return $this;
   }
 
   public function setStoryTime($time) {
     $this->storyTime = $time;
     return $this;
   }
 
   public function setStoryAuthorPHID($phid) {
     $this->storyAuthorPHID = $phid;
     return $this;
   }
 
   public function setMailRecipientPHIDs(array $phids) {
     $this->mailRecipientPHIDs = $phids;
     return $this;
   }
 
   public function publish() {
     $class = $this->storyType;
     if (!$class) {
       throw new Exception(
         pht(
           'Call %s before publishing!',
           'setStoryType()'));
     }
 
     if (!class_exists($class)) {
       throw new Exception(
         pht(
           "Story type must be a valid class name and must subclass %s. ".
           "'%s' is not a loadable class.",
           'PhabricatorFeedStory',
           $class));
     }
 
     if (!is_subclass_of($class, 'PhabricatorFeedStory')) {
       throw new Exception(
         pht(
           "Story type must be a valid class name and must subclass %s. ".
           "'%s' is not a subclass of %s.",
           'PhabricatorFeedStory',
           $class,
           'PhabricatorFeedStory'));
     }
 
     $chrono_key = $this->generateChronologicalKey();
 
     $story = new PhabricatorFeedStoryData();
     $story->setStoryType($this->storyType);
     $story->setStoryData($this->storyData);
     $story->setAuthorPHID((string)$this->storyAuthorPHID);
     $story->setChronologicalKey($chrono_key);
     $story->save();
 
     if ($this->relatedPHIDs) {
       $ref = new PhabricatorFeedStoryReference();
 
       $sql = array();
       $conn = $ref->establishConnection('w');
       foreach (array_unique($this->relatedPHIDs) as $phid) {
         $sql[] = qsprintf(
           $conn,
           '(%s, %s)',
           $phid,
           $chrono_key);
       }
 
       queryfx(
         $conn,
         'INSERT INTO %T (objectPHID, chronologicalKey) VALUES %Q',
         $ref->getTableName(),
         implode(', ', $sql));
     }
 
     $subscribed_phids = $this->subscribedPHIDs;
     if ($subscribed_phids) {
       $subscribed_phids = $this->filterSubscribedPHIDs($subscribed_phids);
       $this->insertNotifications($chrono_key, $subscribed_phids);
       $this->sendNotification($chrono_key, $subscribed_phids);
     }
 
     PhabricatorWorker::scheduleTask(
       'FeedPublisherWorker',
       array(
         'key' => $chrono_key,
       ));
 
     return $story;
   }
 
   private function insertNotifications($chrono_key, array $subscribed_phids) {
     if (!$this->primaryObjectPHID) {
       throw new Exception(
         pht(
           'You must call %s if you %s!',
           'setPrimaryObjectPHID()',
           'setSubscribedPHIDs()'));
     }
 
     $notif = new PhabricatorFeedStoryNotification();
     $sql = array();
     $conn = $notif->establishConnection('w');
 
     $will_receive_mail = array_fill_keys($this->mailRecipientPHIDs, true);
 
     foreach (array_unique($subscribed_phids) as $user_phid) {
       if (isset($will_receive_mail[$user_phid])) {
         $mark_read = 1;
       } else {
         $mark_read = 0;
       }
 
       $sql[] = qsprintf(
         $conn,
         '(%s, %s, %s, %d)',
         $this->primaryObjectPHID,
         $user_phid,
         $chrono_key,
         $mark_read);
     }
 
     if ($sql) {
       queryfx(
         $conn,
         'INSERT INTO %T '.
         '(primaryObjectPHID, userPHID, chronologicalKey, hasViewed) '.
         'VALUES %Q',
         $notif->getTableName(),
         implode(', ', $sql));
     }
   }
 
   private function sendNotification($chrono_key, array $subscribed_phids) {
     $data = array(
       'key'         => (string)$chrono_key,
       'type'        => 'notification',
       'subscribers' => $subscribed_phids,
     );
 
     PhabricatorNotificationClient::tryToPostMessage($data);
   }
 
   /**
    * Remove PHIDs who should not receive notifications from a subscriber list.
    *
    * @param list<phid> List of potential subscribers.
    * @return list<phid> List of actual subscribers.
    */
   private function filterSubscribedPHIDs(array $phids) {
     $phids = $this->expandRecipients($phids);
 
     $tags = $this->getMailTags();
     if ($tags) {
       $all_prefs = id(new PhabricatorUserPreferences())->loadAllWhere(
         'userPHID in (%Ls)',
         $phids);
       $all_prefs = mpull($all_prefs, null, 'getUserPHID');
     }
 
     $pref_default = PhabricatorUserPreferences::MAILTAG_PREFERENCE_EMAIL;
     $pref_ignore = PhabricatorUserPreferences::MAILTAG_PREFERENCE_IGNORE;
 
     $keep = array();
     foreach ($phids as $phid) {
       if (($phid == $this->storyAuthorPHID) && !$this->getNotifyAuthor()) {
         continue;
       }
 
       if ($tags && isset($all_prefs[$phid])) {
         $mailtags = $all_prefs[$phid]->getPreference(
           PhabricatorUserPreferences::PREFERENCE_MAILTAGS,
           array());
 
         $notify = false;
         foreach ($tags as $tag) {
           // If this is set to "email" or "notify", notify the user.
           if ((int)idx($mailtags, $tag, $pref_default) != $pref_ignore) {
             $notify = true;
             break;
           }
         }
 
         if (!$notify) {
           continue;
         }
       }
 
       $keep[] = $phid;
     }
 
     return array_values(array_unique($keep));
   }
 
   private function expandRecipients(array $phids) {
     return id(new PhabricatorMetaMTAMemberQuery())
       ->setViewer(PhabricatorUser::getOmnipotentUser())
       ->withPHIDs($phids)
       ->executeExpansion();
   }
 
   /**
    * We generate a unique chronological key for each story type because we want
    * to be able to page through the stream with a cursor (i.e., select stories
    * after ID = X) so we can efficiently perform filtering after selecting data,
    * and multiple stories with the same ID make this cumbersome without putting
    * a bunch of logic in the client. We could use the primary key, but that
    * would prevent publishing stories which happened in the past. Since it's
    * potentially useful to do that (e.g., if you're importing another data
    * source) build a unique key for each story which has chronological ordering.
    *
    * @return string A unique, time-ordered key which identifies the story.
    */
   private function generateChronologicalKey() {
     // Use the epoch timestamp for the upper 32 bits of the key. Default to
     // the current time if the story doesn't have an explicit timestamp.
     $time = nonempty($this->storyTime, time());
 
     // Generate a random number for the lower 32 bits of the key.
     $rand = head(unpack('L', Filesystem::readRandomBytes(4)));
 
     // On 32-bit machines, we have to get creative.
     if (PHP_INT_SIZE < 8) {
       // We're on a 32-bit machine.
       if (function_exists('bcadd')) {
         // Try to use the 'bc' extension.
         return bcadd(bcmul($time, bcpow(2, 32)), $rand);
       } else {
         // Do the math in MySQL. TODO: If we formalize a bc dependency, get
         // rid of this.
         $conn_r = id(new PhabricatorFeedStoryData())->establishConnection('r');
         $result = queryfx_one(
           $conn_r,
           'SELECT (%d << 32) + %d as N',
           $time,
           $rand);
         return $result['N'];
       }
     } else {
       // This is a 64 bit machine, so we can just do the math.
       return ($time << 32) + $rand;
     }
   }
 }
diff --git a/src/applications/feed/builder/PhabricatorFeedBuilder.php b/src/applications/feed/builder/PhabricatorFeedBuilder.php
index 8fbc99df9..941419a33 100644
--- a/src/applications/feed/builder/PhabricatorFeedBuilder.php
+++ b/src/applications/feed/builder/PhabricatorFeedBuilder.php
@@ -1,105 +1,106 @@
 <?php
 
-final class PhabricatorFeedBuilder {
+final class PhabricatorFeedBuilder extends Phobject {
 
+  private $user;
   private $stories;
   private $framed;
   private $hovercards = false;
   private $noDataString;
 
   public function __construct(array $stories) {
     assert_instances_of($stories, 'PhabricatorFeedStory');
     $this->stories = $stories;
   }
 
   public function setFramed($framed) {
     $this->framed = $framed;
     return $this;
   }
 
   public function setUser(PhabricatorUser $user) {
     $this->user = $user;
     return $this;
   }
 
   public function setShowHovercards($hover) {
     $this->hovercards = $hover;
     return $this;
   }
 
   public function setNoDataString($string) {
     $this->noDataString = $string;
     return $this;
   }
 
   public function buildView() {
     if (!$this->user) {
       throw new PhutilInvalidStateException('setUser');
     }
 
     $user = $this->user;
     $stories = $this->stories;
 
     $null_view = new AphrontNullView();
 
     require_celerity_resource('phabricator-feed-css');
 
     $last_date = null;
     foreach ($stories as $story) {
       $story->setFramed($this->framed);
       $story->setHovercard($this->hovercards);
 
       $date = ucfirst(phabricator_relative_date($story->getEpoch(), $user));
 
       if ($date !== $last_date) {
         if ($last_date !== null) {
           $null_view->appendChild(
             phutil_tag_div('phabricator-feed-story-date-separator'));
         }
         $last_date = $date;
         $header = new PHUIHeaderView();
         $header->setHeader($date);
         $header->setHeaderIcon('fa-calendar msr');
 
         $null_view->appendChild($header);
       }
 
       try {
         $view = $story->renderView();
         $view->setUser($user);
         $view = $view->render();
       } catch (Exception $ex) {
         // If rendering failed for any reason, don't fail the entire feed,
         // just this one story.
         $view = id(new PHUIFeedStoryView())
           ->setUser($user)
           ->setChronologicalKey($story->getChronologicalKey())
           ->setEpoch($story->getEpoch())
           ->setTitle(
             pht('Feed Story Failed to Render (%s)', get_class($story)))
           ->appendChild(pht('%s: %s', get_class($ex), $ex->getMessage()));
       }
 
       $null_view->appendChild($view);
     }
 
     $box = id(new PHUIObjectBoxView())
       ->appendChild($null_view);
 
     if (empty($stories)) {
       $nodatastring = pht('No Stories.');
       if ($this->noDataString) {
         $nodatastring = $this->noDataString;
       }
 
       $view = id(new PHUIBoxView())
         ->addClass('mlt mlb msr msl')
         ->appendChild($nodatastring);
       $box->appendChild($view);
     }
 
     return $box;
 
   }
 
 }
diff --git a/src/applications/feed/config/PhabricatorFeedConfigOptions.php b/src/applications/feed/config/PhabricatorFeedConfigOptions.php
index 2ee6a7bca..a3f7bf522 100644
--- a/src/applications/feed/config/PhabricatorFeedConfigOptions.php
+++ b/src/applications/feed/config/PhabricatorFeedConfigOptions.php
@@ -1,58 +1,60 @@
 <?php
 
 final class PhabricatorFeedConfigOptions
   extends PhabricatorApplicationConfigOptions {
 
   public function getName() {
     return pht('Feed');
   }
 
   public function getDescription() {
     return pht('Feed options.');
   }
 
   public function getFontIcon() {
     return 'fa-newspaper-o';
   }
 
   public function getGroup() {
     return 'apps';
   }
 
   public function getOptions() {
     return array(
       $this->newOption('feed.public', 'bool', false)
         ->setLocked(true)
         ->setBoolOptions(
           array(
             pht('Allow anyone to view the feed'),
             pht('Require authentication'),
           ))
         ->setSummary(pht('Should the feed be public?'))
         ->setDescription(
           pht(
             "If you set this to true, you can embed Phabricator activity ".
             "feeds in other pages using iframes. These feeds are completely ".
             "public, and a login is not required to view them! This is ".
             "intended for things like open source projects that want to ".
             "expose an activity feed on the project homepage.\n\n".
-            "NOTE: You must also set `policy.allow-public` to true for this ".
-            "setting to work properly.")),
+            "NOTE: You must also set `%s` to true for this ".
+            "setting to work properly.",
+            'policy.allow-public')),
       $this->newOption('feed.http-hooks', 'list<string>', array())
         ->setLocked(true)
         ->setSummary(pht('POST notifications of feed events.'))
         ->setDescription(
           pht(
-            "If you set this to a list of http URIs, when a feed story is ".
-            "published a task will be created for each uri that posts the ".
-            "story data to the uri. Daemons automagically retry failures 100 ".
-            "times, waiting \$fail_count * 60s between each subsequent ".
-            "failure. Be sure to keep the daemon console (/daemon/) open ".
+            "If you set this to a list of HTTP URIs, when a feed story is ".
+            "published a task will be created for each URI that posts the ".
+            "story data to the URI. Daemons automagically retry failures 100 ".
+            "times, waiting `\$fail_count * 60s` between each subsequent ".
+            "failure. Be sure to keep the daemon console (`%s`) open ".
             "while developing and testing your end points. You may need to".
-            "restart your daemons to start sending http requests.\n\n".
-            "NOTE: URIs are not validated, the URI must return http status ".
-            "200 within 30 seconds, and no permission checks are performed.")),
+            "restart your daemons to start sending HTTP requests.\n\n".
+            "NOTE: URIs are not validated, the URI must return HTTP status ".
+            "200 within 30 seconds, and no permission checks are performed.",
+            '/daemon/')),
     );
   }
 
 }
diff --git a/src/applications/feed/query/PhabricatorFeedQuery.php b/src/applications/feed/query/PhabricatorFeedQuery.php
index b9c7d099a..13cfb266e 100644
--- a/src/applications/feed/query/PhabricatorFeedQuery.php
+++ b/src/applications/feed/query/PhabricatorFeedQuery.php
@@ -1,127 +1,127 @@
 <?php
 
 final class PhabricatorFeedQuery
   extends PhabricatorCursorPagedPolicyAwareQuery {
 
   private $filterPHIDs;
   private $chronologicalKeys;
 
   public function setFilterPHIDs(array $phids) {
     $this->filterPHIDs = $phids;
     return $this;
   }
 
   public function withChronologicalKeys(array $keys) {
     $this->chronologicalKeys = $keys;
     return $this;
   }
 
   protected function loadPage() {
     $story_table = new PhabricatorFeedStoryData();
     $conn = $story_table->establishConnection('r');
 
     $data = queryfx_all(
       $conn,
       'SELECT story.* FROM %T story %Q %Q %Q %Q %Q',
       $story_table->getTableName(),
       $this->buildJoinClause($conn),
       $this->buildWhereClause($conn),
       $this->buildGroupClause($conn),
       $this->buildOrderClause($conn),
       $this->buildLimitClause($conn));
 
     return $data;
   }
 
   protected function willFilterPage(array $data) {
     return PhabricatorFeedStory::loadAllFromRows($data, $this->getViewer());
   }
 
   protected function buildJoinClause(AphrontDatabaseConnection $conn_r) {
     // NOTE: We perform this join unconditionally (even if we have no filter
     // PHIDs) to omit rows which have no story references. These story data
     // rows are notifications or realtime alerts.
 
     $ref_table = new PhabricatorFeedStoryReference();
     return qsprintf(
       $conn_r,
       'JOIN %T ref ON ref.chronologicalKey = story.chronologicalKey',
       $ref_table->getTableName());
   }
 
   protected function buildWhereClause(AphrontDatabaseConnection $conn_r) {
     $where = array();
 
     if ($this->filterPHIDs) {
       $where[] = qsprintf(
         $conn_r,
         'ref.objectPHID IN (%Ls)',
         $this->filterPHIDs);
     }
 
     if ($this->chronologicalKeys) {
       // NOTE: We want to use integers in the query so we can take advantage
       // of keys, but can't use %d on 32-bit systems. Make sure all the keys
       // are integers and then format them raw.
 
       $keys = $this->chronologicalKeys;
       foreach ($keys as $key) {
         if (!ctype_digit($key)) {
           throw new Exception(
             pht("Key '%s' is not a valid chronological key!", $key));
         }
       }
 
       $where[] = qsprintf(
         $conn_r,
         'ref.chronologicalKey IN (%Q)',
         implode(', ', $keys));
     }
 
     $where[] = $this->buildPagingClause($conn_r);
 
     return $this->formatWhereClause($where);
   }
 
   protected function buildGroupClause(AphrontDatabaseConnection $conn_r) {
     if ($this->filterPHIDs) {
       return qsprintf($conn_r, 'GROUP BY ref.chronologicalKey');
     } else {
       return qsprintf($conn_r, 'GROUP BY story.chronologicalKey');
     }
   }
 
   protected function getDefaultOrderVector() {
     return array('key');
   }
 
   public function getOrderableColumns() {
     $table = ($this->filterPHIDs ? 'ref' : 'story');
     return array(
       'key' => array(
         'table' => $table,
         'column' => 'chronologicalKey',
-        'type' => 'int',
+        'type' => 'string',
         'unique' => true,
       ),
     );
   }
 
   protected function getPagingValueMap($cursor, array $keys) {
     return array(
       'key' => $cursor,
     );
   }
 
   protected function getResultCursor($item) {
     if ($item instanceof PhabricatorFeedStory) {
       return $item->getChronologicalKey();
     }
     return $item['chronologicalKey'];
   }
 
   public function getQueryApplicationClass() {
     return 'PhabricatorFeedApplication';
   }
 
 }
diff --git a/src/applications/feed/story/PhabricatorFeedStory.php b/src/applications/feed/story/PhabricatorFeedStory.php
index 253757b8d..03b55df5d 100644
--- a/src/applications/feed/story/PhabricatorFeedStory.php
+++ b/src/applications/feed/story/PhabricatorFeedStory.php
@@ -1,534 +1,535 @@
 <?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
+  extends Phobject
   implements
     PhabricatorPolicyInterface,
     PhabricatorMarkupInterface {
 
   private $data;
   private $hasViewed;
   private $framed;
   private $hovercard = false;
   private $renderingTarget = PhabricatorApplicationTransaction::TARGET_HTML;
 
   private $handles = array();
   private $objects = array();
   private $projectPHIDs = array();
   private $markupFieldOutput = 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, __CLASS__);
       } 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);
     }
 
     // Load and process story markup blocks.
 
     $engine = new PhabricatorMarkupEngine();
     $engine->setViewer($viewer);
     foreach ($stories as $story) {
       foreach ($story->getFieldStoryMarkupFields() as $field) {
         $engine->addObject($story, $field);
       }
     }
 
     $engine->process();
 
     foreach ($stories as $story) {
       foreach ($story->getFieldStoryMarkupFields() as $field) {
         $story->setMarkupFieldOutput(
           $field,
           $engine->getOutput($story, $field));
       }
     }
 
     return $stories;
   }
 
   public function setMarkupFieldOutput($field, $output) {
     $this->markupFieldOutput[$field] = $output;
     return $this;
   }
 
   public function getMarkupFieldOutput($field) {
     if (!array_key_exists($field, $this->markupFieldOutput)) {
       throw new Exception(
         pht(
           'Trying to retrieve markup field key "%s", but this feed story '.
           'did not request it be rendered.',
           $field));
     }
 
     return $this->markupFieldOutput[$field];
   }
 
   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(pht('Unknown rendering target: %s', $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(
         pht(
           "Story is asking for an object it did not request ('%s')!",
           $phid));
     }
     return $object;
   }
 
   public function getPrimaryObject() {
     $phid = $this->getPrimaryObjectPHID();
     if (!$phid) {
       throw new Exception(pht('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 renderAsTextForDoorkeeper(
     DoorkeeperFeedStoryPublisher $publisher) {
 
     // TODO: This (and text rendering) should be properly abstract and
     // universal. However, this is far less bad than it used to be, and we
     // need to clean up more old feed code to really make this reasonable.
 
     return pht(
       '(Unable to render story of class %s for Doorkeeper.)',
       get_class($this));
   }
 
   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(pht("Unloaded Object '%s'", $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 public function renderSummary($text, $len = 128) {
     if ($len) {
       $text = id(new PhutilUTF8StringTruncator())
         ->setMaximumGlyphs($len)
         ->truncateString($text);
     }
     switch ($this->getRenderingTarget()) {
       case PhabricatorApplicationTransaction::TARGET_HTML:
         $text = phutil_escape_html_newlines($text);
         break;
     }
     return $text;
   }
 
   public function getNotificationAggregations() {
     return array();
   }
 
   protected function newStoryView() {
     $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;
   }
 
   public function getFieldStoryMarkupFields() {
     return array();
   }
 
 
 /* -(  PhabricatorPolicyInterface Implementation  )-------------------------- */
 
   public function getPHID() {
     return null;
   }
 
   /**
    * @task policy
    */
   public function getCapabilities() {
     return array(
       PhabricatorPolicyCapability::CAN_VIEW,
     );
   }
 
 
   /**
    * @task policy
    */
   public function getPolicy($capability) {
     $policy_object = $this->getPrimaryPolicyObject();
     if ($policy_object) {
       return $policy_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) {
     $policy_object = $this->getPrimaryPolicyObject();
     if ($policy_object) {
       return $policy_object->hasAutomaticCapability($capability, $viewer);
     }
 
     return false;
   }
 
   public function describeAutomaticCapability($capability) {
     return null;
   }
 
 
   /**
    * Get the policy object this story is about, if such a policy object
    * exists.
    *
    * @return PhabricatorPolicyInterface|null Policy object, if available.
    * @task policy
    */
   private function getPrimaryPolicyObject() {
     $primary_phid = $this->getPrimaryObjectPHID();
     if (empty($this->objects[$primary_phid])) {
       $object = $this->objects[$primary_phid];
       if ($object instanceof PhabricatorPolicyInterface) {
         return $object;
       }
     }
 
     return null;
   }
 
 
 /* -(  PhabricatorMarkupInterface Implementation )--------------------------- */
 
 
   public function getMarkupFieldKey($field) {
     return 'feed:'.$this->getChronologicalKey().':'.$field;
   }
 
   public function newMarkupEngine($field) {
     return PhabricatorMarkupEngine::newMarkupEngine(array());
   }
 
   public function getMarkupText($field) {
     throw new PhutilMethodNotImplementedException();
   }
 
   public function didMarkupText(
     $field,
     $output,
     PhutilMarkupEngine $engine) {
     return $output;
   }
 
   public function shouldUseMarkupCache($field) {
     return true;
   }
 
 }
diff --git a/src/applications/files/PhabricatorImageTransformer.php b/src/applications/files/PhabricatorImageTransformer.php
index 829f5d9f2..75e83c223 100644
--- a/src/applications/files/PhabricatorImageTransformer.php
+++ b/src/applications/files/PhabricatorImageTransformer.php
@@ -1,396 +1,396 @@
 <?php
 
 /**
  * @task enormous Detecting Enormous Images
  * @task save     Saving Image Data
  */
-final class PhabricatorImageTransformer {
+final class PhabricatorImageTransformer extends Phobject {
 
   public function executeMemeTransform(
     PhabricatorFile $file,
     $upper_text,
     $lower_text) {
     $image = $this->applyMemeToFile($file, $upper_text, $lower_text);
     return PhabricatorFile::newFromFileData(
       $image,
       array(
         'name' => 'meme-'.$file->getName(),
         'ttl' => time() + 60 * 60 * 24,
         'canCDN' => true,
       ));
   }
 
   public function executeConpherenceTransform(
     PhabricatorFile $file,
     $top,
     $left,
     $width,
     $height) {
 
     $image = $this->crasslyCropTo(
       $file,
       $top,
       $left,
       $width,
       $height);
 
     return PhabricatorFile::newFromFileData(
       $image,
       array(
         'name' => 'conpherence-'.$file->getName(),
         'profile' => true,
         'canCDN' => true,
       ));
   }
 
   private function crasslyCropTo(PhabricatorFile $file, $top, $left, $w, $h) {
     $data = $file->loadFileData();
     $src = imagecreatefromstring($data);
     $dst = $this->getBlankDestinationFile($w, $h);
 
     $scale = self::getScaleForCrop($file, $w, $h);
     $orig_x = $left / $scale;
     $orig_y = $top / $scale;
     $orig_w = $w / $scale;
     $orig_h = $h / $scale;
 
     imagecopyresampled(
       $dst,
       $src,
       0, 0,
       $orig_x, $orig_y,
       $w, $h,
       $orig_w, $orig_h);
 
     return self::saveImageDataInAnyFormat($dst, $file->getMimeType());
   }
 
   private function getBlankDestinationFile($dx, $dy) {
     $dst = imagecreatetruecolor($dx, $dy);
     imagesavealpha($dst, true);
     imagefill($dst, 0, 0, imagecolorallocatealpha($dst, 255, 255, 255, 127));
 
     return $dst;
   }
 
   public static function getScaleForCrop(
     PhabricatorFile $file,
     $des_width,
     $des_height) {
 
     $metadata = $file->getMetadata();
     $width = $metadata[PhabricatorFile::METADATA_IMAGE_WIDTH];
     $height = $metadata[PhabricatorFile::METADATA_IMAGE_HEIGHT];
 
     if ($height < $des_height) {
       $scale = $height / $des_height;
     } else if ($width < $des_width) {
       $scale = $width / $des_width;
     } else {
       $scale_x = $des_width / $width;
       $scale_y = $des_height / $height;
       $scale = max($scale_x, $scale_y);
     }
 
     return $scale;
   }
 
   private function applyMemeToFile(
     PhabricatorFile $file,
     $upper_text,
     $lower_text) {
     $data = $file->loadFileData();
 
     $img_type = $file->getMimeType();
     $imagemagick = PhabricatorEnv::getEnvConfig('files.enable-imagemagick');
 
     if ($img_type != 'image/gif' || $imagemagick == false) {
       return $this->applyMemeTo(
         $data, $upper_text, $lower_text, $img_type);
     }
 
     $data = $file->loadFileData();
     $input = new TempFile();
     Filesystem::writeFile($input, $data);
 
     list($out) = execx('convert %s info:', $input);
     $split = phutil_split_lines($out);
     if (count($split) > 1) {
       return $this->applyMemeWithImagemagick(
         $input,
         $upper_text,
         $lower_text,
         count($split),
         $img_type);
     } else {
       return $this->applyMemeTo($data, $upper_text, $lower_text, $img_type);
     }
   }
 
   private function applyMemeTo(
     $data,
     $upper_text,
     $lower_text,
     $mime_type) {
     $img = imagecreatefromstring($data);
 
     // Some PNGs have color palettes, and allocating the dark border color
     // fails and gives us whatever's first in the color table. Copy the image
     // to a fresh truecolor canvas before working with it.
 
     $truecolor = imagecreatetruecolor(imagesx($img), imagesy($img));
     imagecopy($truecolor, $img, 0, 0, 0, 0, imagesx($img), imagesy($img));
     $img = $truecolor;
 
     $phabricator_root = dirname(phutil_get_library_root('phabricator'));
     $font_root = $phabricator_root.'/resources/font/';
     $font_path = $font_root.'tuffy.ttf';
     if (Filesystem::pathExists($font_root.'impact.ttf')) {
       $font_path = $font_root.'impact.ttf';
     }
     $text_color = imagecolorallocate($img, 255, 255, 255);
     $border_color = imagecolorallocatealpha($img, 0, 0, 0, 110);
     $border_width = 4;
     $font_max = 200;
     $font_min = 5;
     for ($i = $font_max; $i > $font_min; $i--) {
       $fit = $this->doesTextBoundingBoxFitInImage(
         $img,
         $upper_text,
         $i,
         $font_path);
       if ($fit['doesfit']) {
         $x = ($fit['imgwidth'] - $fit['txtwidth']) / 2;
         $y = $fit['txtheight'] + 10;
         $this->makeImageWithTextBorder($img,
           $i,
           $x,
           $y,
           $text_color,
           $border_color,
           $border_width,
           $font_path,
           $upper_text);
         break;
       }
     }
     for ($i = $font_max; $i > $font_min; $i--) {
       $fit = $this->doesTextBoundingBoxFitInImage($img,
         $lower_text, $i, $font_path);
       if ($fit['doesfit']) {
         $x = ($fit['imgwidth'] - $fit['txtwidth']) / 2;
         $y = $fit['imgheight'] - 10;
         $this->makeImageWithTextBorder(
           $img,
           $i,
           $x,
           $y,
           $text_color,
           $border_color,
           $border_width,
           $font_path,
           $lower_text);
         break;
       }
     }
     return self::saveImageDataInAnyFormat($img, $mime_type);
   }
 
   private function makeImageWithTextBorder($img, $font_size, $x, $y,
     $color, $stroke_color, $bw, $font, $text) {
     $angle = 0;
     $bw = abs($bw);
     for ($c1 = $x - $bw; $c1 <= $x + $bw; $c1++) {
       for ($c2 = $y - $bw; $c2 <= $y + $bw; $c2++) {
         if (!(($c1 == $x - $bw || $x + $bw) &&
           $c2 == $y - $bw || $c2 == $y + $bw)) {
           $bg = imagettftext($img, $font_size,
             $angle, $c1, $c2, $stroke_color, $font, $text);
           }
         }
       }
     imagettftext($img, $font_size, $angle,
             $x , $y, $color , $font, $text);
   }
 
   private function doesTextBoundingBoxFitInImage($img,
     $text, $font_size, $font_path) {
     // Default Angle = 0
     $angle = 0;
 
     $bbox = imagettfbbox($font_size, $angle, $font_path, $text);
     $text_height = abs($bbox[3] - $bbox[5]);
     $text_width = abs($bbox[0] - $bbox[2]);
     return array(
       'doesfit' => ($text_height * 1.05 <= imagesy($img) / 2
         && $text_width * 1.05 <= imagesx($img)),
       'txtwidth' => $text_width,
       'txtheight' => $text_height,
       'imgwidth' => imagesx($img),
       'imgheight' => imagesy($img),
     );
   }
 
   private function applyMemeWithImagemagick(
     $input,
     $above,
     $below,
     $count,
     $img_type) {
 
     $output = new TempFile();
     $future = new ExecFuture(
       'convert %s -coalesce +adjoin %s_%s',
       $input,
       $input,
       '%09d');
     $future->setTimeout(10)->resolvex();
 
     $output_files = array();
     for ($ii = 0; $ii < $count; $ii++) {
       $frame_name = sprintf('%s_%09d', $input, $ii);
       $output_name = sprintf('%s_%09d', $output, $ii);
 
       $output_files[] = $output_name;
 
       $frame_data = Filesystem::readFile($frame_name);
       $memed_frame_data = $this->applyMemeTo(
         $frame_data,
         $above,
         $below,
         $img_type);
       Filesystem::writeFile($output_name, $memed_frame_data);
     }
 
     $future = new ExecFuture('convert -loop 0 %Ls %s', $output_files, $output);
     $future->setTimeout(10)->resolvex();
 
     return Filesystem::readFile($output);
   }
 
 
 /* -(  Saving Image Data  )-------------------------------------------------- */
 
 
   /**
    * Save an image resource to a string representation suitable for storage or
    * transmission as an image file.
    *
    * Optionally, you can specify a preferred MIME type like `"image/png"`.
    * Generally, you should specify the MIME type of the original file if you're
    * applying file transformations. The MIME type may not be honored if
    * Phabricator can not encode images in the given format (based on available
    * extensions), but can save images in another format.
    *
    * @param   resource  GD image resource.
    * @param   string?   Optionally, preferred mime type.
    * @return  string    Bytes of an image file.
    * @task save
    */
   public static function saveImageDataInAnyFormat($data, $preferred_mime = '') {
     $preferred = null;
     switch ($preferred_mime) {
       case 'image/gif':
         $preferred = self::saveImageDataAsGIF($data);
         break;
       case 'image/png':
         $preferred = self::saveImageDataAsPNG($data);
         break;
     }
 
     if ($preferred !== null) {
       return $preferred;
     }
 
     $data = self::saveImageDataAsJPG($data);
     if ($data !== null) {
       return $data;
     }
 
     $data = self::saveImageDataAsPNG($data);
     if ($data !== null) {
       return $data;
     }
 
     $data = self::saveImageDataAsGIF($data);
     if ($data !== null) {
       return $data;
     }
 
     throw new Exception(pht('Failed to save image data into any format.'));
   }
 
 
   /**
    * Save an image in PNG format, returning the file data as a string.
    *
    * @param resource      GD image resource.
    * @return string|null  PNG file as a string, or null on failure.
    * @task save
    */
   private static function saveImageDataAsPNG($image) {
     if (!function_exists('imagepng')) {
       return null;
     }
 
     ob_start();
     $result = imagepng($image, null, 9);
     $output = ob_get_clean();
 
     if (!$result) {
       return null;
     }
 
     return $output;
   }
 
 
   /**
    * Save an image in GIF format, returning the file data as a string.
    *
    * @param resource      GD image resource.
    * @return string|null  GIF file as a string, or null on failure.
    * @task save
    */
   private static function saveImageDataAsGIF($image) {
     if (!function_exists('imagegif')) {
       return null;
     }
 
     ob_start();
     $result = imagegif($image);
     $output = ob_get_clean();
 
     if (!$result) {
       return null;
     }
 
     return $output;
   }
 
 
   /**
    * Save an image in JPG format, returning the file data as a string.
    *
    * @param resource      GD image resource.
    * @return string|null  JPG file as a string, or null on failure.
    * @task save
    */
   private static function saveImageDataAsJPG($image) {
     if (!function_exists('imagejpeg')) {
       return null;
     }
 
     ob_start();
     $result = imagejpeg($image);
     $output = ob_get_clean();
 
     if (!$result) {
       return null;
     }
 
     return $output;
   }
 
 
 }
diff --git a/src/applications/files/application/PhabricatorFilesApplication.php b/src/applications/files/application/PhabricatorFilesApplication.php
index b80a8e1ae..1714719f8 100644
--- a/src/applications/files/application/PhabricatorFilesApplication.php
+++ b/src/applications/files/application/PhabricatorFilesApplication.php
@@ -1,114 +1,115 @@
 <?php
 
 final class PhabricatorFilesApplication extends PhabricatorApplication {
 
   public function getBaseURI() {
     return '/file/';
   }
 
   public function getName() {
     return pht('Files');
   }
 
   public function getShortDescription() {
     return pht('Store and Share Files');
   }
 
   public function getFontIcon() {
     return 'fa-file';
   }
 
   public function getTitleGlyph() {
     return "\xE2\x87\xAA";
   }
 
   public function getFlavorText() {
     return pht('Blob store for Pokemon pictures.');
   }
 
   public function getApplicationGroup() {
     return self::GROUP_UTILITIES;
   }
 
   public function canUninstall() {
     return false;
   }
 
   public function getRemarkupRules() {
     return array(
       new PhabricatorEmbedFileRemarkupRule(),
     );
   }
 
   public function supportsEmailIntegration() {
     return true;
   }
 
   public function getAppEmailBlurb() {
     return pht(
       'Send emails with file attachments to these addresses to upload '.
       'files. %s',
       phutil_tag(
         'a',
         array(
           'href' => $this->getInboundEmailSupportLink(),
         ),
         pht('Learn More')));
   }
 
   protected function getCustomCapabilities() {
     return array(
       FilesDefaultViewCapability::CAPABILITY => array(
         'caption' => pht('Default view policy for newly created files.'),
+        'template' => PhabricatorFileFilePHIDType::TYPECONST,
       ),
     );
   }
 
   public function getRoutes() {
     return array(
       '/F(?P<id>[1-9]\d*)' => 'PhabricatorFileInfoController',
       '/file/' => array(
         '(query/(?P<key>[^/]+)/)?' => 'PhabricatorFileListController',
         'upload/' => 'PhabricatorFileUploadController',
         'dropupload/' => 'PhabricatorFileDropUploadController',
         'compose/' => 'PhabricatorFileComposeController',
         'comment/(?P<id>[1-9]\d*)/' => 'PhabricatorFileCommentController',
         'delete/(?P<id>[1-9]\d*)/' => 'PhabricatorFileDeleteController',
         'edit/(?P<id>[1-9]\d*)/' => 'PhabricatorFileEditController',
         'info/(?P<phid>[^/]+)/' => 'PhabricatorFileInfoController',
         'data/'.
           '(?:@(?P<instance>[^/]+)/)?'.
           '(?P<key>[^/]+)/'.
           '(?P<phid>[^/]+)/'.
           '(?:(?P<token>[^/]+)/)?'.
           '.*'
           => 'PhabricatorFileDataController',
         'proxy/' => 'PhabricatorFileProxyController',
         'xform/'.
           '(?:@(?P<instance>[^/]+)/)?'.
           '(?P<transform>[^/]+)/'.
           '(?P<phid>[^/]+)/'.
           '(?P<key>[^/]+)/'
           => 'PhabricatorFileTransformController',
         'transforms/(?P<id>[1-9]\d*)/' =>
           'PhabricatorFileTransformListController',
         'uploaddialog/' => 'PhabricatorFileUploadDialogController',
         'download/(?P<phid>[^/]+)/' => 'PhabricatorFileDialogController',
       ),
     );
   }
 
   public function getMailCommandObjects() {
     return array(
       'file' => array(
         'name' => pht('Email Commands: Files'),
         'header' => pht('Interacting with Files'),
         'object' => new PhabricatorFile(),
         'summary' => pht(
           'This page documents the commands you can use to interact with '.
           'files.'),
       ),
     );
   }
 
 }
diff --git a/src/applications/files/engine/PhabricatorFileStorageEngine.php b/src/applications/files/engine/PhabricatorFileStorageEngine.php
index be2fb2bbd..3ec0ee2fa 100644
--- a/src/applications/files/engine/PhabricatorFileStorageEngine.php
+++ b/src/applications/files/engine/PhabricatorFileStorageEngine.php
@@ -1,369 +1,369 @@
 <?php
 
 /**
  * Defines a storage engine which can write file data somewhere (like a
  * database, local disk, Amazon S3, the A:\ drive, or a custom filer) and
  * retrieve it later.
  *
  * You can extend this class to provide new file storage backends.
  *
  * For more information, see @{article:File Storage Technical Documentation}.
  *
  * @task construct Constructing an Engine
  * @task meta Engine Metadata
  * @task file Managing File Data
  * @task load Loading Storage Engines
  */
-abstract class PhabricatorFileStorageEngine {
+abstract class PhabricatorFileStorageEngine extends Phobject {
 
   /**
    * Construct a new storage engine.
    *
    * @task construct
    */
   final public function __construct() {
     // <empty>
   }
 
 
 /* -(  Engine Metadata  )---------------------------------------------------- */
 
 
   /**
    * Return a unique, nonempty string which identifies this storage engine.
    * This is used to look up the storage engine when files needs to be read or
    * deleted. For instance, if you store files by giving them to a duck for
    * safe keeping in his nest down by the pond, you might return 'duck' from
    * this method.
    *
    * @return string Unique string for this engine, max length 32.
    * @task meta
    */
   abstract public function getEngineIdentifier();
 
 
   /**
    * Prioritize this engine relative to other engines.
    *
    * Engines with a smaller priority number get an opportunity to write files
    * first. Generally, lower-latency filestores should have lower priority
    * numbers, and higher-latency filestores should have higher priority
    * numbers. Setting priority to approximately the number of milliseconds of
    * read latency will generally produce reasonable results.
    *
    * In conjunction with filesize limits, the goal is to store small files like
    * profile images, thumbnails, and text snippets in lower-latency engines,
    * and store large files in higher-capacity engines.
    *
    * @return float Engine priority.
    * @task meta
    */
   abstract public function getEnginePriority();
 
 
   /**
    * Return `true` if the engine is currently writable.
    *
    * Engines that are disabled or missing configuration should return `false`
    * to prevent new writes. If writes were made with this engine in the past,
    * the application may still try to perform reads.
    *
    * @return bool True if this engine can support new writes.
    * @task meta
    */
   abstract public function canWriteFiles();
 
 
   /**
    * Return `true` if the engine has a filesize limit on storable files.
    *
    * The @{method:getFilesizeLimit} method can retrieve the actual limit. This
    * method just removes the ambiguity around the meaning of a `0` limit.
    *
    * @return bool `true` if the engine has a filesize limit.
    * @task meta
    */
   public function hasFilesizeLimit() {
     return true;
   }
 
 
   /**
    * Return maximum storable file size, in bytes.
    *
    * Not all engines have a limit; use @{method:getFilesizeLimit} to check if
    * an engine has a limit. Engines without a limit can store files of any
    * size.
    *
    * By default, engines define a limit which supports chunked storage of
    * large files. In most cases, you should not change this limit, even if an
    * engine has vast storage capacity: chunked storage makes large files more
    * manageable and enables features like resumable uploads.
    *
    * @return int Maximum storable file size, in bytes.
    * @task meta
    */
   public function getFilesizeLimit() {
     // NOTE: This 8MB limit is selected to be larger than the 4MB chunk size,
     // but not much larger. Files between 0MB and 8MB will be stored normally;
     // files larger than 8MB will be chunked.
     return (1024 * 1024 * 8);
   }
 
 
   /**
    * Identifies storage engines that support unit tests.
    *
    * These engines are not used for production writes.
    *
    * @return bool True if this is a test engine.
    * @task meta
    */
   public function isTestEngine() {
     return false;
   }
 
 
   /**
    * Identifies chunking storage engines.
    *
    * If this is a storage engine which splits files into chunks and stores the
    * chunks in other engines, it can return `true` to signal that other
    * chunking engines should not try to store data here.
    *
    * @return bool True if this is a chunk engine.
    * @task meta
    */
   public function isChunkEngine() {
     return false;
   }
 
 
 /* -(  Managing File Data  )------------------------------------------------- */
 
 
   /**
    * Write file data to the backing storage and return a handle which can later
    * be used to read or delete it. For example, if the backing storage is local
    * disk, the handle could be the path to the file.
    *
    * The caller will provide a $params array, which may be empty or may have
    * some metadata keys (like "name" and "author") in it. You should be prepared
    * to handle writes which specify no metadata, but might want to optionally
    * use some keys in this array for debugging or logging purposes. This is
    * the same dictionary passed to @{method:PhabricatorFile::newFromFileData},
    * so you could conceivably do custom things with it.
    *
    * If you are unable to write for whatever reason (e.g., the disk is full),
    * throw an exception. If there are other satisfactory but less-preferred
    * storage engines available, they will be tried.
    *
    * @param  string The file data to write.
    * @param  array  File metadata (name, author), if available.
    * @return string Unique string which identifies the stored file, max length
    *                255.
    * @task file
    */
   abstract public function writeFile($data, array $params);
 
 
   /**
    * Read the contents of a file previously written by @{method:writeFile}.
    *
    * @param   string  The handle returned from @{method:writeFile} when the
    *                  file was written.
    * @return  string  File contents.
    * @task file
    */
   abstract public function readFile($handle);
 
 
   /**
    * Delete the data for a file previously written by @{method:writeFile}.
    *
    * @param   string  The handle returned from @{method:writeFile} when the
    *                  file was written.
    * @return  void
    * @task file
    */
   abstract public function deleteFile($handle);
 
 
 
 /* -(  Loading Storage Engines  )-------------------------------------------- */
 
 
   /**
    * Select viable default storage engines according to configuration. We'll
    * select the MySQL and Local Disk storage engines if they are configured
    * to allow a given file.
    *
    * @param int File size in bytes.
    * @task load
    */
   public static function loadStorageEngines($length) {
     $engines = self::loadWritableEngines();
 
     $writable = array();
     foreach ($engines as $key => $engine) {
       if ($engine->hasFilesizeLimit()) {
         $limit = $engine->getFilesizeLimit();
         if ($limit < $length) {
           continue;
         }
       }
 
       $writable[$key] = $engine;
     }
 
     return $writable;
   }
 
 
   /**
    * @task load
    */
   public static function loadAllEngines() {
     static $engines;
 
     if ($engines === null) {
       $objects = id(new PhutilSymbolLoader())
         ->setAncestorClass(__CLASS__)
         ->loadObjects();
 
       $map = array();
       foreach ($objects as $engine) {
         $key = $engine->getEngineIdentifier();
         if (empty($map[$key])) {
           $map[$key] = $engine;
         } else {
           throw new Exception(
             pht(
               'Storage engines "%s" and "%s" have the same engine '.
               'identifier "%s". Each storage engine must have a unique '.
               'identifier.',
               get_class($engine),
               get_class($map[$key]),
               $key));
         }
       }
 
       $map = msort($map, 'getEnginePriority');
 
       $engines = $map;
     }
 
     return $engines;
   }
 
 
   /**
    * @task load
    */
   private static function loadProductionEngines() {
     $engines = self::loadAllEngines();
 
     $active = array();
     foreach ($engines as $key => $engine) {
       if ($engine->isTestEngine()) {
         continue;
       }
 
       $active[$key] = $engine;
     }
 
     return $active;
   }
 
 
   /**
    * @task load
    */
   public static function loadWritableEngines() {
     $engines = self::loadProductionEngines();
 
     $writable = array();
     foreach ($engines as $key => $engine) {
       if (!$engine->canWriteFiles()) {
         continue;
       }
 
       if ($engine->isChunkEngine()) {
         // Don't select chunk engines as writable.
         continue;
       }
       $writable[$key] = $engine;
     }
 
     return $writable;
   }
 
   /**
    * @task load
    */
   public static function loadWritableChunkEngines() {
     $engines = self::loadProductionEngines();
 
     $chunk = array();
     foreach ($engines as $key => $engine) {
       if (!$engine->canWriteFiles()) {
         continue;
       }
       if (!$engine->isChunkEngine()) {
         continue;
       }
       $chunk[$key] = $engine;
     }
 
     return $chunk;
   }
 
 
 
   /**
    * Return the largest file size which can not be uploaded in chunks.
    *
    * Files smaller than this will always upload in one request, so clients
    * can safely skip the allocation step.
    *
    * @return int|null Byte size, or `null` if there is no chunk support.
    */
   public static function getChunkThreshold() {
     $engines = self::loadWritableChunkEngines();
 
     $min = null;
     foreach ($engines as $engine) {
       if (!$min) {
         $min = $engine;
         continue;
       }
 
       if ($min->getChunkSize() > $engine->getChunkSize()) {
         $min = $engine->getChunkSize();
       }
     }
 
     if (!$min) {
       return null;
     }
 
     return $engine->getChunkSize();
   }
 
   public function getFileDataIterator(PhabricatorFile $file, $begin, $end) {
     // The default implementation is trivial and just loads the entire file
     // upfront.
     $data = $file->loadFileData();
 
     if ($begin !== null && $end !== null) {
       $data = substr($data, $begin, ($end - $begin));
     } else if ($begin !== null) {
       $data = substr($data, $begin);
     } else if ($end !== null) {
       $data = substr($data, 0, $end);
     }
 
     return array($data);
   }
 
 }
diff --git a/src/applications/files/engine/__tests__/PhabricatorFileStorageEngineTestCase.php b/src/applications/files/engine/__tests__/PhabricatorFileStorageEngineTestCase.php
new file mode 100644
index 000000000..8ffbe0366
--- /dev/null
+++ b/src/applications/files/engine/__tests__/PhabricatorFileStorageEngineTestCase.php
@@ -0,0 +1,10 @@
+<?php
+
+final class PhabricatorFileStorageEngineTestCase extends PhabricatorTestCase {
+
+  public function testLoadAllEngines() {
+    PhabricatorFileStorageEngine::loadAllEngines();
+    $this->assertTrue(true);
+  }
+
+}
diff --git a/src/applications/files/query/PhabricatorFileBundleLoader.php b/src/applications/files/query/PhabricatorFileBundleLoader.php
index 0cc760302..76b90debf 100644
--- a/src/applications/files/query/PhabricatorFileBundleLoader.php
+++ b/src/applications/files/query/PhabricatorFileBundleLoader.php
@@ -1,27 +1,27 @@
 <?php
 
 /**
  * Callback provider for loading @{class@arcanist:ArcanistBundle} file data
  * stored in the Files application.
  */
-final class PhabricatorFileBundleLoader {
+final class PhabricatorFileBundleLoader extends Phobject {
 
   private $viewer;
 
   public function setViewer(PhabricatorUser $viewer) {
     $this->viewer = $viewer;
     return $this;
   }
 
   public function loadFileData($phid) {
     $file = id(new PhabricatorFileQuery())
       ->setViewer($this->viewer)
       ->withPHIDs(array($phid))
       ->executeOne();
     if (!$file) {
       return null;
     }
     return $file->loadFileData();
   }
 
 }
diff --git a/src/applications/files/transform/__tests__/PhabricatorFileTransformTestCase.php b/src/applications/files/transform/__tests__/PhabricatorFileTransformTestCase.php
new file mode 100644
index 000000000..4cc4a6e54
--- /dev/null
+++ b/src/applications/files/transform/__tests__/PhabricatorFileTransformTestCase.php
@@ -0,0 +1,10 @@
+<?php
+
+final class PhabricatorFileTransformTestCase extends PhabricatorTestCase {
+
+  public function testGetAllTransforms() {
+    PhabricatorFileTransform::getAllTransforms();
+    $this->assertTrue(true);
+  }
+
+}
diff --git a/src/applications/flag/constants/PhabricatorFlagConstants.php b/src/applications/flag/constants/PhabricatorFlagConstants.php
index 968443a33..5162b928c 100644
--- a/src/applications/flag/constants/PhabricatorFlagConstants.php
+++ b/src/applications/flag/constants/PhabricatorFlagConstants.php
@@ -1,3 +1,3 @@
 <?php
 
-abstract class PhabricatorFlagConstants {}
+abstract class PhabricatorFlagConstants extends Phobject {}
diff --git a/src/applications/fund/application/PhabricatorFundApplication.php b/src/applications/fund/application/PhabricatorFundApplication.php
index 57997a161..ed36cc783 100644
--- a/src/applications/fund/application/PhabricatorFundApplication.php
+++ b/src/applications/fund/application/PhabricatorFundApplication.php
@@ -1,71 +1,72 @@
 <?php
 
 final class PhabricatorFundApplication extends PhabricatorApplication {
 
   public function getName() {
     return pht('Fund');
   }
 
   public function getBaseURI() {
     return '/fund/';
   }
 
   public function getShortDescription() {
     return pht('Donate');
   }
 
   public function getFontIcon() {
     return 'fa-heart';
   }
 
   public function getTitleGlyph() {
     return "\xE2\x99\xA5";
   }
 
   public function getApplicationGroup() {
     return self::GROUP_UTILITIES;
   }
 
   public function isPrototype() {
     return true;
   }
 
   public function getRemarkupRules() {
     return array(
       new FundInitiativeRemarkupRule(),
     );
   }
 
   public function getRoutes() {
     return array(
       '/I(?P<id>[1-9]\d*)' => 'FundInitiativeViewController',
       '/fund/' => array(
         '(?:query/(?P<queryKey>[^/]+)/)?' => 'FundInitiativeListController',
         'create/' => 'FundInitiativeEditController',
         'edit/(?:(?P<id>\d+)/)?' => 'FundInitiativeEditController',
         'close/(?P<id>\d+)/' => 'FundInitiativeCloseController',
         'back/(?P<id>\d+)/' => 'FundInitiativeBackController',
         'backers/(?:(?P<id>\d+)/)?(?:query/(?P<queryKey>[^/]+)/)?'
           => 'FundBackerListController',
       ),
     );
   }
 
   protected function getCustomCapabilities() {
     return array(
       FundDefaultViewCapability::CAPABILITY => array(
         'caption' => pht('Default view policy for newly created initiatives.'),
+        'tempate' => FundInitiativePHIDType::TYPECONST,
       ),
       FundCreateInitiativesCapability::CAPABILITY => array(
         'default' => PhabricatorPolicies::POLICY_ADMIN,
       ),
     );
   }
 
   public function getApplicationSearchDocumentTypes() {
     return array(
       FundInitiativePHIDType::TYPECONST,
     );
   }
 
 }
diff --git a/src/applications/harbormaster/query/HarbormasterBuildLogQuery.php b/src/applications/harbormaster/query/HarbormasterBuildLogQuery.php
index f0f2021fd..3d45de686 100644
--- a/src/applications/harbormaster/query/HarbormasterBuildLogQuery.php
+++ b/src/applications/harbormaster/query/HarbormasterBuildLogQuery.php
@@ -1,98 +1,99 @@
 <?php
 
 final class HarbormasterBuildLogQuery
   extends PhabricatorCursorPagedPolicyAwareQuery {
 
   private $ids;
   private $phids;
   private $buildPHIDs;
+  private $buildTargetPHIDs;
 
   public function withIDs(array $ids) {
     $this->ids = $ids;
     return $this;
   }
 
   public function withPHIDs(array $phids) {
     $this->phids = $phids;
     return $this;
   }
 
   public function withBuildTargetPHIDs(array $build_target_phids) {
     $this->buildTargetPHIDs = $build_target_phids;
     return $this;
   }
 
   protected function loadPage() {
     $table = new HarbormasterBuildLog();
     $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) {
     $build_targets = array();
 
     $build_target_phids = array_filter(mpull($page, 'getBuildTargetPHID'));
     if ($build_target_phids) {
       $build_targets = id(new HarbormasterBuildTargetQuery())
         ->setViewer($this->getViewer())
         ->withPHIDs($build_target_phids)
         ->setParentQuery($this)
         ->execute();
       $build_targets = mpull($build_targets, null, 'getPHID');
     }
 
     foreach ($page as $key => $build_log) {
       $build_target_phid = $build_log->getBuildTargetPHID();
       if (empty($build_targets[$build_target_phid])) {
         unset($page[$key]);
         continue;
       }
       $build_log->attachBuildTarget($build_targets[$build_target_phid]);
     }
 
     return $page;
   }
 
   protected 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->buildTargetPHIDs) {
       $where[] = qsprintf(
         $conn_r,
         'buildTargetPHID IN (%Ls)',
         $this->buildTargetPHIDs);
     }
 
     $where[] = $this->buildPagingClause($conn_r);
 
     return $this->formatWhereClause($where);
   }
 
   public function getQueryApplicationClass() {
     return 'PhabricatorHarbormasterApplication';
   }
 
 }
diff --git a/src/applications/harbormaster/step/HarbormasterBuildStepImplementation.php b/src/applications/harbormaster/step/HarbormasterBuildStepImplementation.php
index 5262366ac..26450722b 100644
--- a/src/applications/harbormaster/step/HarbormasterBuildStepImplementation.php
+++ b/src/applications/harbormaster/step/HarbormasterBuildStepImplementation.php
@@ -1,250 +1,250 @@
 <?php
 
-abstract class HarbormasterBuildStepImplementation {
+abstract class HarbormasterBuildStepImplementation extends Phobject {
 
   public static function getImplementations() {
     return id(new PhutilSymbolLoader())
       ->setAncestorClass(__CLASS__)
       ->loadObjects();
   }
 
   public static function getImplementation($class) {
     $base = idx(self::getImplementations(), $class);
 
     if ($base) {
       return (clone $base);
     }
 
     return null;
   }
 
   public static function requireImplementation($class) {
     if (!$class) {
       throw new Exception(pht('No implementation is specified!'));
     }
 
     $implementation = self::getImplementation($class);
     if (!$implementation) {
       throw new Exception(pht('No such implementation "%s" exists!', $class));
     }
 
     return $implementation;
   }
 
   /**
    * The name of the implementation.
    */
   abstract public function getName();
 
   /**
    * The generic description of the implementation.
    */
   public function getGenericDescription() {
     return '';
   }
 
   /**
    * The description of the implementation, based on the current settings.
    */
   public function getDescription() {
     return $this->getGenericDescription();
   }
 
   /**
    * Run the build target against the specified build.
    */
   abstract public function execute(
     HarbormasterBuild $build,
     HarbormasterBuildTarget $build_target);
 
   /**
    * Gets the settings for this build step.
    */
   public function getSettings() {
     return $this->settings;
   }
 
   public function getSetting($key, $default = null) {
     return idx($this->settings, $key, $default);
   }
 
   /**
    * Loads the settings for this build step implementation from a build
    * step or target.
    */
   final public function loadSettings($build_object) {
     $this->settings = $build_object->getDetails();
     return $this;
   }
 
   /**
    * Return the name of artifacts produced by this command.
    *
    * Something like:
    *
    *   return array(
    *     'some_name_input_by_user' => 'host');
    *
    * Future steps will calculate all available artifact mappings
    * before them and filter on the type.
    *
    * @return array The mappings of artifact names to their types.
    */
   public function getArtifactInputs() {
     return array();
   }
 
   public function getArtifactOutputs() {
     return array();
   }
 
   public function getDependencies(HarbormasterBuildStep $build_step) {
     $dependencies = $build_step->getDetail('dependsOn', array());
 
     $inputs = $build_step->getStepImplementation()->getArtifactInputs();
     $inputs = ipull($inputs, null, 'key');
 
     $artifacts = $this->getAvailableArtifacts(
       $build_step->getBuildPlan(),
       $build_step,
       null);
 
     foreach ($artifacts as $key => $type) {
       if (!array_key_exists($key, $inputs)) {
         unset($artifacts[$key]);
       }
     }
 
     $artifact_steps = ipull($artifacts, 'step');
     $artifact_steps = mpull($artifact_steps, 'getPHID');
 
     $dependencies = array_merge($dependencies, $artifact_steps);
 
     return $dependencies;
   }
 
   /**
    * Returns a list of all artifacts made available in the build plan.
    */
   public static function getAvailableArtifacts(
     HarbormasterBuildPlan $build_plan,
     $current_build_step,
     $artifact_type) {
 
     $steps = id(new HarbormasterBuildStepQuery())
       ->setViewer(PhabricatorUser::getOmnipotentUser())
       ->withBuildPlanPHIDs(array($build_plan->getPHID()))
       ->execute();
 
     $artifacts = array();
 
     $artifact_arrays = array();
     foreach ($steps as $step) {
       if ($current_build_step !== null &&
         $step->getPHID() === $current_build_step->getPHID()) {
 
         continue;
       }
 
       $implementation = $step->getStepImplementation();
       $array = $implementation->getArtifactOutputs();
       $array = ipull($array, 'type', 'key');
       foreach ($array as $name => $type) {
         if ($type !== $artifact_type && $artifact_type !== null) {
           continue;
         }
         $artifacts[$name] = array('type' => $type, 'step' => $step);
       }
     }
 
     return $artifacts;
   }
 
   /**
    * Convert a user-provided string with variables in it, like:
    *
    *   ls ${dirname}
    *
    * ...into a string with variables merged into it safely:
    *
    *   ls 'dir with spaces'
    *
    * @param string Name of a `vxsprintf` function, like @{function:vcsprintf}.
    * @param string User-provided pattern string containing `${variables}`.
    * @param dict   List of available replacement variables.
    * @return string String with variables replaced safely into it.
    */
   protected function mergeVariables($function, $pattern, array $variables) {
     $regexp = '/\\$\\{(?P<name>[a-z\\.]+)\\}/';
 
     $matches = null;
     preg_match_all($regexp, $pattern, $matches);
 
     $argv = array();
     foreach ($matches['name'] as $name) {
       if (!array_key_exists($name, $variables)) {
         throw new Exception(pht("No such variable '%s'!", $name));
       }
       $argv[] = $variables[$name];
     }
 
     $pattern = str_replace('%', '%%', $pattern);
     $pattern = preg_replace($regexp, '%s', $pattern);
 
     return call_user_func($function, $pattern, $argv);
   }
 
   public function getFieldSpecifications() {
     return array();
   }
 
   protected function formatSettingForDescription($key, $default = null) {
     return $this->formatValueForDescription($this->getSetting($key, $default));
   }
 
   protected function formatValueForDescription($value) {
     if (strlen($value)) {
       return phutil_tag('strong', array(), $value);
     } else {
       return phutil_tag('em', array(), pht('(null)'));
     }
   }
 
   public function supportsWaitForMessage() {
     return false;
   }
 
   public function shouldWaitForMessage(HarbormasterBuildTarget $target) {
     if (!$this->supportsWaitForMessage()) {
       return false;
     }
 
     return (bool)$target->getDetail('builtin.wait-for-message');
   }
 
   protected function shouldAbort(
     HarbormasterBuild $build,
     HarbormasterBuildTarget $target) {
 
     return $build->getBuildGeneration() !== $target->getBuildGeneration();
   }
 
   protected function resolveFuture(
     HarbormasterBuild $build,
     HarbormasterBuildTarget $target,
     Future $future) {
 
     $futures = new FutureIterator(array($future));
     foreach ($futures->setUpdateInterval(5) as $key => $future) {
       if ($future === null) {
         $build->reload();
         if ($this->shouldAbort($build, $target)) {
           throw new HarbormasterBuildAbortedException();
         }
       } else {
         return $future->resolve();
       }
     }
   }
 
 }
diff --git a/src/applications/harbormaster/step/HarbormasterHTTPRequestBuildStepImplementation.php b/src/applications/harbormaster/step/HarbormasterHTTPRequestBuildStepImplementation.php
index dfc29cedc..5d361b3de 100644
--- a/src/applications/harbormaster/step/HarbormasterHTTPRequestBuildStepImplementation.php
+++ b/src/applications/harbormaster/step/HarbormasterHTTPRequestBuildStepImplementation.php
@@ -1,109 +1,109 @@
 <?php
 
 final class HarbormasterHTTPRequestBuildStepImplementation
   extends HarbormasterBuildStepImplementation {
 
   public function getName() {
     return pht('Make HTTP Request');
   }
 
   public function getGenericDescription() {
     return pht('Make an HTTP request.');
   }
 
   public function getDescription() {
     $domain = null;
     $uri = $this->getSetting('uri');
     if ($uri) {
       $domain = id(new PhutilURI($uri))->getDomain();
     }
 
     $method = $this->formatSettingForDescription('method', 'POST');
     $domain = $this->formatValueForDescription($domain);
 
     if ($this->getSetting('credential')) {
       return pht(
         'Make an authenticated HTTP %s request to %s.',
         $method,
         $domain);
     } else {
       return pht(
         'Make an HTTP %s request to %s.',
         $method,
         $domain);
     }
   }
 
   public function execute(
     HarbormasterBuild $build,
     HarbormasterBuildTarget $build_target) {
 
     $viewer = PhabricatorUser::getOmnipotentUser();
     $settings = $this->getSettings();
     $variables = $build_target->getVariables();
 
     $uri = $this->mergeVariables(
       'vurisprintf',
       $settings['uri'],
       $variables);
 
     $log_body = $build->createLog($build_target, $uri, 'http-body');
     $start = $log_body->start();
 
     $method = nonempty(idx($settings, 'method'), 'POST');
 
     $future = id(new HTTPSFuture($uri))
       ->setMethod($method)
       ->setTimeout(60);
 
     $credential_phid = $this->getSetting('credential');
     if ($credential_phid) {
       $key = PassphrasePasswordKey::loadFromPHID(
         $credential_phid,
         $viewer);
       $future->setHTTPBasicAuthCredentials(
         $key->getUsernameEnvelope()->openEnvelope(),
         $key->getPasswordEnvelope());
     }
 
     list($status, $body, $headers) = $this->resolveFuture(
       $build,
       $build_target,
       $future);
 
     $log_body->append($body);
     $log_body->finalize($start);
 
     if ($status->getStatusCode() != 200) {
       $build->setBuildStatus(HarbormasterBuild::STATUS_FAILED);
     }
   }
 
   public function getFieldSpecifications() {
     return array(
       'uri' => array(
         'name' => pht('URI'),
         'type' => 'text',
         'required' => true,
       ),
       'method' => array(
         'name' => pht('HTTP Method'),
         'type' => 'select',
         'options' => array_fuse(array('POST', 'GET', 'PUT', 'DELETE')),
       ),
       'credential' => array(
         'name' => pht('Credentials'),
         'type' => 'credential',
         'credential.type'
-          => PassphraseCredentialTypePassword::CREDENTIAL_TYPE,
+          => PassphrasePasswordCredentialType::CREDENTIAL_TYPE,
         'credential.provides'
-          => PassphraseCredentialTypePassword::PROVIDES_TYPE,
+          => PassphrasePasswordCredentialType::PROVIDES_TYPE,
       ),
     );
   }
 
   public function supportsWaitForMessage() {
     return true;
   }
 
 }
diff --git a/src/applications/harbormaster/step/__tests__/HarbormasterBuildStepImplementationTestCase.php b/src/applications/harbormaster/step/__tests__/HarbormasterBuildStepImplementationTestCase.php
new file mode 100644
index 000000000..6a37b0112
--- /dev/null
+++ b/src/applications/harbormaster/step/__tests__/HarbormasterBuildStepImplementationTestCase.php
@@ -0,0 +1,11 @@
+<?php
+
+final class HarbormasterBuildStepImplementationTestCase
+  extends PhabricatorTestCase {
+
+  public function testGetImplementations() {
+    HarbormasterBuildStepImplementation::getImplementations();
+    $this->assertTrue(true);
+  }
+
+}
diff --git a/src/applications/herald/adapter/HeraldAdapter.php b/src/applications/herald/adapter/HeraldAdapter.php
index f50bdf420..01014aded 100644
--- a/src/applications/herald/adapter/HeraldAdapter.php
+++ b/src/applications/herald/adapter/HeraldAdapter.php
@@ -1,1780 +1,1799 @@
 <?php
 
 /**
  * @task customfield Custom Field Integration
  */
-abstract class HeraldAdapter {
+abstract class HeraldAdapter extends Phobject {
 
   const FIELD_TITLE                  = 'title';
   const FIELD_BODY                   = 'body';
   const FIELD_AUTHOR                 = 'author';
   const FIELD_ASSIGNEE               = 'assignee';
   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_DIFF_ENORMOUS          = 'diff-enormous';
   const FIELD_REPOSITORY             = 'repository';
   const FIELD_REPOSITORY_PROJECTS    = 'repository-projects';
   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 FIELD_PROJECTS               = 'projects';
   const FIELD_PUSHER                 = 'pusher';
   const FIELD_PUSHER_PROJECTS        = 'pusher-projects';
   const FIELD_DIFFERENTIAL_REVISION  = 'differential-revision';
   const FIELD_DIFFERENTIAL_REVIEWERS = 'differential-reviewers';
   const FIELD_DIFFERENTIAL_CCS       = 'differential-ccs';
   const FIELD_DIFFERENTIAL_ACCEPTED  = 'differential-accepted';
   const FIELD_IS_MERGE_COMMIT        = 'is-merge-commit';
   const FIELD_BRANCHES               = 'branches';
   const FIELD_AUTHOR_RAW             = 'author-raw';
   const FIELD_COMMITTER_RAW          = 'committer-raw';
   const FIELD_IS_NEW_OBJECT          = 'new-object';
   const FIELD_APPLICATION_EMAIL      = 'applicaton-email';
   const FIELD_TASK_PRIORITY          = 'taskpriority';
   const FIELD_TASK_STATUS            = 'taskstatus';
   const FIELD_PUSHER_IS_COMMITTER    = 'pusher-is-committer';
   const FIELD_PATH                   = 'path';
+  const FIELD_SPACE = 'space';
 
   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_NEVER           = 'never';
   const CONDITION_REGEXP_PAIR     = 'regexp-pair';
   const CONDITION_HAS_BIT         = 'bit';
   const CONDITION_NOT_BIT         = '!bit';
   const CONDITION_IS_TRUE         = 'true';
   const CONDITION_IS_FALSE        = 'false';
 
   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_REMOVE_PROJECTS = 'removeprojects';
   const ACTION_ADD_REVIEWERS = 'addreviewers';
   const ACTION_ADD_BLOCKING_REVIEWERS = 'addblockingreviewers';
   const ACTION_APPLY_BUILD_PLANS = 'applybuildplans';
   const ACTION_BLOCK = 'block';
   const ACTION_REQUIRE_SIGNATURE = 'signature';
 
   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';
   const VALUE_TASK_PRIORITY   = 'taskpriority';
   const VALUE_TASK_STATUS     = 'taskstatus';
   const VALUE_LEGAL_DOCUMENTS   = 'legaldocuments';
   const VALUE_APPLICATION_EMAIL = 'applicationemail';
+  const VALUE_SPACE = 'space';
 
   private $contentSource;
   private $isNewObject;
   private $applicationEmail;
   private $customFields = false;
   private $customActions = null;
   private $queuedTransactions = array();
   private $emailPHIDs = array();
   private $forcedEmailPHIDs = array();
   private $unsubscribedPHIDs;
 
   public function getEmailPHIDs() {
     return array_values($this->emailPHIDs);
   }
 
   public function getForcedEmailPHIDs() {
     return array_values($this->forcedEmailPHIDs);
   }
 
   public function getCustomActions() {
     if ($this->customActions === null) {
       $custom_actions = id(new PhutilSymbolLoader())
         ->setAncestorClass('HeraldCustomAction')
         ->loadObjects();
 
       foreach ($custom_actions as $key => $object) {
         if (!$object->appliesToAdapter($this)) {
           unset($custom_actions[$key]);
         }
       }
 
       $this->customActions = array();
       foreach ($custom_actions as $action) {
         $key = $action->getActionKey();
 
         if (array_key_exists($key, $this->customActions)) {
           throw new Exception(
             pht(
               "More than one Herald custom action implementation ".
               "handles the action key: '%s'.",
               $key));
         }
 
         $this->customActions[$key] = $action;
       }
     }
 
     return $this->customActions;
   }
 
   public function setContentSource(PhabricatorContentSource $content_source) {
     $this->contentSource = $content_source;
     return $this;
   }
   public function getContentSource() {
     return $this->contentSource;
   }
 
   public function getIsNewObject() {
     if (is_bool($this->isNewObject)) {
       return $this->isNewObject;
     }
 
     throw new Exception(
       pht(
         'You must %s to a boolean first!',
         'setIsNewObject()'));
   }
   public function setIsNewObject($new) {
     $this->isNewObject = (bool)$new;
     return $this;
   }
 
   public function setApplicationEmail(
     PhabricatorMetaMTAApplicationEmail $email) {
     $this->applicationEmail = $email;
     return $this;
   }
 
   public function getApplicationEmail() {
     return $this->applicationEmail;
   }
 
   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;
       case self::FIELD_IS_NEW_OBJECT:
         return $this->getIsNewObject();
       case self::FIELD_CC:
         $object = $this->getObject();
 
         if (!($object instanceof PhabricatorSubscribableInterface)) {
           throw new Exception(
             pht(
               'Adapter object (of class "%s") does not implement interface '.
               '"%s", so the subscribers field value can not be determined.',
               get_class($object),
               'PhabricatorSubscribableInterface'));
         }
 
         $phid = $object->getPHID();
         return PhabricatorSubscribersQuery::loadSubscribersForPHID($phid);
       case self::FIELD_APPLICATION_EMAIL:
         $value = array();
         // while there is only one match by implementation, we do set
         // comparisons on phids, so return an array with just the phid
         if ($this->getApplicationEmail()) {
           $value[] = $this->getApplicationEmail()->getPHID();
         }
         return $value;
+      case self::FIELD_SPACE:
+        $object = $this->getObject();
+
+        if (!($object instanceof PhabricatorSpacesInterface)) {
+          throw new Exception(
+            pht(
+              'Adapter object (of class "%s") does not implement interface '.
+              '"%s", so the Space field value can not be determined.',
+              get_class($object),
+              'PhabricatorSpacesInterface'));
+        }
+
+        return PhabricatorSpacesNamespaceQuery::getObjectSpacePHID($object);
       default:
         if ($this->isHeraldCustomKey($field_name)) {
           return $this->getCustomFieldValue($field_name);
         }
 
         throw new Exception(pht("Unknown field '%s'!", $field_name));
     }
   }
 
   abstract public function applyHeraldEffects(array $effects);
 
   protected function handleCustomHeraldEffect(HeraldEffect $effect) {
     $custom_action = idx($this->getCustomActions(), $effect->getAction());
 
     if ($custom_action !== null) {
       return $custom_action->applyEffect(
         $this,
         $this->getObject(),
         $effect);
     }
 
     return null;
   }
 
   public function isAvailableToUser(PhabricatorUser $viewer) {
     $applications = id(new PhabricatorApplicationQuery())
       ->setViewer($viewer)
       ->withInstalled(true)
       ->withClasses(array($this->getAdapterApplicationClass()))
       ->execute();
 
     return !empty($applications);
   }
 
   public function queueTransaction($transaction) {
     $this->queuedTransactions[] = $transaction;
   }
 
   public function getQueuedTransactions() {
     return $this->queuedTransactions;
   }
 
   protected function newTransaction() {
     $object = $this->newObject();
 
     if (!($object instanceof PhabricatorApplicationTransactionInterface)) {
       throw new Exception(
         pht(
           'Unable to build a new transaction for adapter object; it does '.
           'not implement "%s".',
           'PhabricatorApplicationTransactionInterface'));
     }
 
     return $object->getApplicationTransactionTemplate();
   }
 
 
   /**
    * 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 getAdapterContentDescription();
   abstract public function getAdapterApplicationClass();
   abstract public function getObject();
 
 
   /**
    * Return a new characteristic object for this adapter.
    *
    * The adapter will use this object to test for interfaces, generate
    * transactions, and interact with custom fields.
    *
    * Adapters must return an object from this method to enable custom
    * field rules and various implicit actions.
    *
    * Normally, you'll return an empty version of the adapted object:
    *
    *   return new ApplicationObject();
    *
    * @return null|object Template object.
    */
   protected function newObject() {
     return null;
   }
 
   public function supportsRuleType($rule_type) {
     return false;
   }
 
   public function canTriggerOnObject($object) {
     return false;
   }
 
   public function explainValidTriggerObjects() {
     return pht('This adapter can not trigger on objects.');
   }
 
   public function getTriggerObjectPHIDs() {
     return array($this->getPHID());
   }
 
   public function getAdapterSortKey() {
     return sprintf(
       '%08d%s',
       $this->getAdapterSortOrder(),
       $this->getAdapterContentName());
   }
 
   public function getAdapterSortOrder() {
     return 1000;
   }
 
 
 /* -(  Fields  )------------------------------------------------------------- */
 
 
   public function getFields() {
     $fields = array();
 
     $fields[] = self::FIELD_ALWAYS;
     $fields[] = self::FIELD_RULE;
 
     $custom_fields = $this->getCustomFields();
     if ($custom_fields) {
       foreach ($custom_fields->getFields() as $custom_field) {
         $key = $custom_field->getFieldKey();
         $fields[] = $this->getHeraldKeyFromCustomKey($key);
       }
     }
 
     return $fields;
   }
 
   public function getFieldNameMap() {
     return array(
       self::FIELD_TITLE => pht('Title'),
       self::FIELD_BODY => pht('Body'),
       self::FIELD_AUTHOR => pht('Author'),
       self::FIELD_ASSIGNEE => pht('Assignee'),
       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_DIFF_ENORMOUS => pht('Change is enormous'),
       self::FIELD_REPOSITORY => pht('Repository'),
       self::FIELD_REPOSITORY_PROJECTS => pht('Repository\'s projects'),
       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"),
       self::FIELD_PROJECTS => pht('Projects'),
       self::FIELD_PUSHER => pht('Pusher'),
       self::FIELD_PUSHER_PROJECTS => pht("Pusher's projects"),
       self::FIELD_DIFFERENTIAL_REVISION => pht('Differential revision'),
       self::FIELD_DIFFERENTIAL_REVIEWERS => pht('Differential reviewers'),
       self::FIELD_DIFFERENTIAL_CCS => pht('Differential CCs'),
       self::FIELD_DIFFERENTIAL_ACCEPTED
         => pht('Accepted Differential revision'),
       self::FIELD_IS_MERGE_COMMIT => pht('Commit is a merge'),
       self::FIELD_BRANCHES => pht('Commit\'s branches'),
       self::FIELD_AUTHOR_RAW => pht('Raw author name'),
       self::FIELD_COMMITTER_RAW => pht('Raw committer name'),
       self::FIELD_IS_NEW_OBJECT => pht('Is newly created?'),
       self::FIELD_APPLICATION_EMAIL => pht('Receiving email address'),
       self::FIELD_TASK_PRIORITY => pht('Task priority'),
       self::FIELD_TASK_STATUS => pht('Task status'),
       self::FIELD_PUSHER_IS_COMMITTER => pht('Pusher same as committer'),
       self::FIELD_PATH => pht('Path'),
+      self::FIELD_SPACE => pht('Space'),
     ) + $this->getCustomFieldNameMap();
   }
 
 
 /* -(  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_TRUE         => pht('is true'),
       self::CONDITION_IS_FALSE        => pht('is false'),
       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_NEVER           => '',  // don't show anything!
       self::CONDITION_REGEXP_PAIR     => pht('matches regexp pair'),
       self::CONDITION_HAS_BIT         => pht('has bit'),
       self::CONDITION_NOT_BIT         => pht('lacks bit'),
     );
   }
 
   public function getConditionsForField($field) {
     switch ($field) {
       case self::FIELD_TITLE:
       case self::FIELD_BODY:
       case self::FIELD_COMMITTER_RAW:
       case self::FIELD_AUTHOR_RAW:
       case self::FIELD_PATH:
         return array(
           self::CONDITION_CONTAINS,
           self::CONDITION_NOT_CONTAINS,
           self::CONDITION_IS,
           self::CONDITION_IS_NOT,
           self::CONDITION_REGEXP,
         );
       case self::FIELD_REVIEWER:
       case self::FIELD_PUSHER:
       case self::FIELD_TASK_PRIORITY:
       case self::FIELD_TASK_STATUS:
+      case self::FIELD_SPACE:
         return array(
           self::CONDITION_IS_ANY,
           self::CONDITION_IS_NOT_ANY,
         );
       case self::FIELD_REPOSITORY:
       case self::FIELD_ASSIGNEE:
       case self::FIELD_AUTHOR:
       case self::FIELD_COMMITTER:
         return array(
           self::CONDITION_IS_ANY,
           self::CONDITION_IS_NOT_ANY,
           self::CONDITION_EXISTS,
           self::CONDITION_NOT_EXISTS,
         );
       case self::FIELD_TAGS:
       case self::FIELD_REVIEWERS:
       case self::FIELD_CC:
       case self::FIELD_AUTHOR_PROJECTS:
       case self::FIELD_PROJECTS:
       case self::FIELD_AFFECTED_PACKAGE:
       case self::FIELD_AFFECTED_PACKAGE_OWNER:
       case self::FIELD_PUSHER_PROJECTS:
       case self::FIELD_REPOSITORY_PROJECTS:
         return array(
           self::CONDITION_INCLUDE_ALL,
           self::CONDITION_INCLUDE_ANY,
           self::CONDITION_INCLUDE_NONE,
           self::CONDITION_EXISTS,
           self::CONDITION_NOT_EXISTS,
         );
       case self::FIELD_APPLICATION_EMAIL:
         return array(
           self::CONDITION_INCLUDE_ANY,
           self::CONDITION_INCLUDE_NONE,
           self::CONDITION_EXISTS,
           self::CONDITION_NOT_EXISTS,
         );
       case self::FIELD_DIFF_FILE:
       case self::FIELD_BRANCHES:
         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_CONTENT_SOURCE:
         return array(
           self::CONDITION_IS,
           self::CONDITION_IS_NOT,
         );
       case self::FIELD_ALWAYS:
         return array(
           self::CONDITION_UNCONDITIONALLY,
         );
       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:
       case self::FIELD_DIFFERENTIAL_ACCEPTED:
         return array(
           self::CONDITION_EXISTS,
           self::CONDITION_NOT_EXISTS,
         );
       case self::FIELD_IS_MERGE_COMMIT:
       case self::FIELD_DIFF_ENORMOUS:
       case self::FIELD_IS_NEW_OBJECT:
       case self::FIELD_PUSHER_IS_COMMITTER:
         return array(
           self::CONDITION_IS_TRUE,
           self::CONDITION_IS_FALSE,
         );
       default:
         if ($this->isHeraldCustomKey($field)) {
           return $this->getCustomFieldConditions($field);
         }
         throw new Exception(
           pht(
             "This adapter does not define conditions for field '%s'!",
             $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(
             pht('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(
             pht('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(
             pht('Object produced non-array value!'));
         }
         if (!is_array($condition_value)) {
           throw new HeraldInvalidConditionException(
             pht('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:
       case self::CONDITION_IS_TRUE:
         return (bool)$field_value;
       case self::CONDITION_NOT_EXISTS:
       case self::CONDITION_IS_FALSE:
         return !$field_value;
       case self::CONDITION_UNCONDITIONALLY:
         return (bool)$field_value;
       case self::CONDITION_NEVER:
         return false;
       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(
               pht('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 = null;
         try {
           $regexp_pair = phutil_json_decode($condition_value);
         } catch (PhutilJSONParserException $ex) {
           throw new HeraldInvalidConditionException(
             pht('Regular expression pair is not valid JSON!'));
         }
         if (count($regexp_pair) != 2) {
           throw new HeraldInvalidConditionException(
             pht('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(
               pht('First regular expression is invalid!'));
           }
           if ($key_matches) {
             $value_matches = @preg_match($value_regexp, $value);
             if ($value_matches === false) {
               throw new HeraldInvalidConditionException(
                 pht('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(
             pht('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;
       case self::CONDITION_HAS_BIT:
         return (($condition_value & $field_value) === (int)$condition_value);
       case self::CONDITION_NOT_BIT:
         return (($condition_value & $field_value) !== (int)$condition_value);
       default:
         throw new HeraldInvalidConditionException(
           pht("Unknown condition '%s'.", $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 = null;
         try {
           $json = phutil_json_decode($condition_value);
         } catch (PhutilJSONParserException $ex) {
           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:
       case self::CONDITION_NEVER:
       case self::CONDITION_HAS_BIT:
       case self::CONDITION_NOT_BIT:
       case self::CONDITION_IS_TRUE:
       case self::CONDITION_IS_FALSE:
         // 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  )------------------------------------------------------------ */
 
   public function getCustomActionsForRuleType($rule_type) {
     $results = array();
     foreach ($this->getCustomActions() as $custom_action) {
       if ($custom_action->appliesToRuleType($rule_type)) {
         $results[] = $custom_action;
       }
     }
     return $results;
   }
 
   public function getActions($rule_type) {
     $custom_actions = $this->getCustomActionsForRuleType($rule_type);
     $custom_actions = mpull($custom_actions, 'getActionKey');
 
     $actions = $custom_actions;
 
     $object = $this->newObject();
 
     if (($object instanceof PhabricatorProjectInterface)) {
       if ($rule_type == HeraldRuleTypeConfig::RULE_TYPE_GLOBAL) {
         $actions[] = self::ACTION_ADD_PROJECTS;
         $actions[] = self::ACTION_REMOVE_PROJECTS;
       }
     }
 
     return $actions;
   }
 
   public function getActionNameMap($rule_type) {
     switch ($rule_type) {
       case HeraldRuleTypeConfig::RULE_TYPE_GLOBAL:
       case HeraldRuleTypeConfig::RULE_TYPE_OBJECT:
         $standard = 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_REMOVE_PROJECTS => pht('Remove projects'),
           self::ACTION_ADD_REVIEWERS => pht('Add reviewers'),
           self::ACTION_ADD_BLOCKING_REVIEWERS => pht('Add blocking reviewers'),
           self::ACTION_APPLY_BUILD_PLANS => pht('Run build plans'),
           self::ACTION_REQUIRE_SIGNATURE => pht('Require legal signatures'),
           self::ACTION_BLOCK => pht('Block change with message'),
         );
         break;
       case HeraldRuleTypeConfig::RULE_TYPE_PERSONAL:
         $standard = 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_REVIEWERS => pht('Add me as a reviewer'),
           self::ACTION_ADD_BLOCKING_REVIEWERS =>
             pht('Add me as a blocking reviewer'),
         );
         break;
       default:
         throw new Exception(pht("Unknown rule type '%s'!", $rule_type));
     }
 
     $custom_actions = $this->getCustomActionsForRuleType($rule_type);
     $standard += mpull($custom_actions, 'getActionName', 'getActionKey');
 
     return $standard;
   }
 
   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_BLOCK:
         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) {
 
     if ($this->isHeraldCustomKey($field)) {
       $value_type = $this->getCustomFieldValueTypeForFieldAndCondition(
         $field,
         $condition);
       if ($value_type !== null) {
         return $value_type;
       }
     }
 
     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;
           case self::FIELD_TASK_PRIORITY:
             return self::VALUE_TASK_PRIORITY;
           case self::FIELD_TASK_STATUS:
             return self::VALUE_TASK_STATUS;
+          case self::FIELD_SPACE:
+            return self::VALUE_SPACE;
           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:
           case self::FIELD_PUSHER_PROJECTS:
           case self::FIELD_PROJECTS:
           case self::FIELD_REPOSITORY_PROJECTS:
             return self::VALUE_PROJECT;
           case self::FIELD_REVIEWERS:
             return self::VALUE_USER_OR_PROJECT;
           case self::FIELD_APPLICATION_EMAIL:
             return self::VALUE_APPLICATION_EMAIL;
           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:
       case self::CONDITION_NEVER:
       case self::CONDITION_IS_TRUE:
       case self::CONDITION_IS_FALSE:
         return self::VALUE_NONE;
       case self::CONDITION_RULE:
       case self::CONDITION_NOT_RULE:
         return self::VALUE_RULE;
       default:
         throw new Exception(pht("Unknown condition '%s'.", $condition));
     }
   }
 
   public 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:
         case self::ACTION_REMOVE_PROJECTS:
           return self::VALUE_PROJECT;
       }
     } 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_ADD_PROJECTS:
         case self::ACTION_REMOVE_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_AUDIT:
         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;
         case self::ACTION_REQUIRE_SIGNATURE:
           return self::VALUE_LEGAL_DOCUMENTS;
         case self::ACTION_BLOCK:
           return self::VALUE_TEXT;
       }
     }
 
     $custom_action = idx($this->getCustomActions(), $action);
     if ($custom_action !== null) {
       return $custom_action->getActionType();
     }
 
     throw new Exception(pht("Unknown or invalid action '%s'.", $action));
   }
 
 
 /* -(  Repetition  )--------------------------------------------------------- */
 
 
   public function getRepetitionOptions() {
     return array(
       HeraldRepetitionPolicyConfig::EVERY,
     );
   }
 
   public static function getAllAdapters() {
     static $adapters;
     if (!$adapters) {
       $adapters = id(new PhutilSymbolLoader())
         ->setAncestorClass(__CLASS__)
         ->loadObjects();
       $adapters = msort($adapters, 'getAdapterSortKey');
     }
     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 = self::getAllAdapters();
     foreach ($adapters as $adapter) {
       if (!$adapter->isAvailableToUser($viewer)) {
         continue;
       }
       $type = $adapter->getAdapterContentType();
       $name = $adapter->getAdapterContentName();
       $map[$type] = $name;
     }
 
     return $map;
   }
 
   public function renderRuleAsText(
     HeraldRule $rule,
     PhabricatorHandleList $handles) {
 
     require_celerity_resource('herald-css');
 
     $icon = id(new PHUIIconView())
       ->setIconFont('fa-chevron-circle-right lightgreytext')
       ->addClass('herald-list-icon');
 
     if ($rule->getMustMatchAll()) {
       $match_text = pht('When all of these conditions are met:');
     } else {
       $match_text = pht('When any of these conditions are met:');
     }
 
     $match_title = phutil_tag(
       'p',
       array(
         'class' => 'herald-list-description',
       ),
       $match_text);
 
     $match_list = array();
     foreach ($rule->getConditions() as $condition) {
       $match_list[] = phutil_tag(
         'div',
         array(
           'class' => 'herald-list-item',
         ),
         array(
           $icon,
           $this->renderConditionAsText($condition, $handles),
         ));
     }
 
     $integer_code_for_every = HeraldRepetitionPolicyConfig::toInt(
       HeraldRepetitionPolicyConfig::EVERY);
 
     if ($rule->getRepetitionPolicy() == $integer_code_for_every) {
       $action_text =
         pht('Take these actions every time this rule matches:');
     } else {
       $action_text =
         pht('Take these actions the first time this rule matches:');
     }
 
     $action_title = phutil_tag(
       'p',
       array(
         'class' => 'herald-list-description',
       ),
       $action_text);
 
     $action_list = array();
     foreach ($rule->getActions() as $action) {
       $action_list[] = phutil_tag(
         'div',
         array(
           'class' => 'herald-list-item',
         ),
         array(
           $icon,
           $this->renderActionAsText($action, $handles),
         ));
     }
 
     return array(
       $match_title,
       $match_list,
       $action_title,
       $action_list,
     );
   }
 
   private function renderConditionAsText(
     HeraldCondition $condition,
     PhabricatorHandleList $handles) {
 
     $field_type = $condition->getFieldName();
 
     $default = $this->isHeraldCustomKey($field_type)
       ? pht('(Unknown Custom Field "%s")', $field_type)
       : pht('(Unknown Field "%s")', $field_type);
 
     $field_name = idx($this->getFieldNameMap(), $field_type, $default);
 
     $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,
     PhabricatorHandleList $handles) {
     $rule_global = HeraldRuleTypeConfig::RULE_TYPE_GLOBAL;
 
     $action_type = $action->getAction();
 
     $default = $this->isHeraldCustomKey($action_type)
       ? pht('(Unknown Custom Action "%s") equals', $action_type)
       : pht('(Unknown Action "%s") equals', $action_type);
 
     $action_name = idx(
       $this->getActionNameMap($rule_global),
       $action_type,
       $default);
 
     $target = $this->renderActionTargetAsText($action, $handles);
 
     return hsprintf('    %s %s', $action_name, $target);
   }
 
   private function renderConditionValueAsText(
     HeraldCondition $condition,
     PhabricatorHandleList $handles) {
 
     $value = $condition->getValue();
     if (!is_array($value)) {
       $value = array($value);
     }
     switch ($condition->getFieldName()) {
       case self::FIELD_TASK_PRIORITY:
         $priority_map = ManiphestTaskPriority::getTaskPriorityMap();
         foreach ($value as $index => $val) {
           $name = idx($priority_map, $val);
           if ($name) {
             $value[$index] = $name;
           }
         }
         break;
       case self::FIELD_TASK_STATUS:
         $status_map = ManiphestTaskStatus::getTaskStatusMap();
         foreach ($value as $index => $val) {
           $name = idx($status_map, $val);
           if ($name) {
             $value[$index] = $name;
           }
         }
         break;
       case HeraldPreCommitRefAdapter::FIELD_REF_CHANGE:
         $change_map =
           PhabricatorRepositoryPushLog::getHeraldChangeFlagConditionOptions();
         foreach ($value as $index => $val) {
           $name = idx($change_map, $val);
           if ($name) {
             $value[$index] = $name;
           }
         }
         break;
       default:
         foreach ($value as $index => $val) {
           $handle = $handles->getHandleIfExists($val);
           if ($handle) {
             $value[$index] = $handle->renderLink();
           }
         }
         break;
     }
     $value = phutil_implode_html(', ', $value);
     return $value;
   }
 
   private function renderActionTargetAsText(
     HeraldAction $action,
     PhabricatorHandleList $handles) {
 
     $target = $action->getTarget();
     if (!is_array($target)) {
       $target = array($target);
     }
     foreach ($target as $index => $val) {
       switch ($action->getAction()) {
         case self::ACTION_FLAG:
           $target[$index] = PhabricatorFlagColor::getColorName($val);
           break;
         default:
           $handle = $handles->getHandleIfExists($val);
           if ($handle) {
             $target[$index] = $handle->renderLink();
           }
           break;
       }
     }
     $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;
         }
       }
     }
 
     if ($rule->isObjectRule()) {
       $phids[] = $rule->getTriggerObjectPHID();
     }
 
     return $phids;
   }
 
 /* -(  Custom Field Integration  )------------------------------------------- */
 
 
   /**
    * Returns the prefix used to namespace Herald fields which are based on
    * custom fields.
    *
    * @return string Key prefix.
    * @task customfield
    */
   private function getCustomKeyPrefix() {
     return 'herald.custom/';
   }
 
 
   /**
    * Determine if a field key is based on a custom field or a regular internal
    * field.
    *
    * @param string Field key.
    * @return bool True if the field key is based on a custom field.
    * @task customfield
    */
   private function isHeraldCustomKey($key) {
     $prefix = $this->getCustomKeyPrefix();
     return (strncmp($key, $prefix, strlen($prefix)) == 0);
   }
 
 
   /**
    * Convert a custom field key into a Herald field key.
    *
    * @param string Custom field key.
    * @return string Herald field key.
    * @task customfield
    */
   private function getHeraldKeyFromCustomKey($key) {
     return $this->getCustomKeyPrefix().$key;
   }
 
 
   /**
    * Get custom fields for this adapter, if appliable. This will either return
    * a field list or `null` if the adapted object does not implement custom
    * fields or the adapter does not support them.
    *
    * @return PhabricatorCustomFieldList|null List of fields, or `null`.
    * @task customfield
    */
   private function getCustomFields() {
     if ($this->customFields === false) {
       $this->customFields = null;
 
 
       $template_object = $this->newObject();
       if ($template_object instanceof PhabricatorCustomFieldInterface) {
         $object = $this->getObject();
         if (!$object) {
           $object = $template_object;
         }
 
         $fields = PhabricatorCustomField::getObjectFields(
           $object,
           PhabricatorCustomField::ROLE_HERALD);
         $fields->setViewer(PhabricatorUser::getOmnipotentUser());
         $fields->readFieldsFromStorage($object);
 
         $this->customFields = $fields;
       }
     }
 
     return $this->customFields;
   }
 
 
   /**
    * Get a custom field by Herald field key, or `null` if it does not exist
    * or custom fields are not supported.
    *
    * @param string Herald field key.
    * @return PhabricatorCustomField|null Matching field, if it exists.
    * @task customfield
    */
   private function getCustomField($herald_field_key) {
     $fields = $this->getCustomFields();
     if (!$fields) {
       return null;
     }
 
     foreach ($fields->getFields() as $custom_field) {
       $key = $custom_field->getFieldKey();
       if ($this->getHeraldKeyFromCustomKey($key) == $herald_field_key) {
         return $custom_field;
       }
     }
 
     return null;
   }
 
 
   /**
    * Get the field map for custom fields.
    *
    * @return map<string, string> Map of Herald field keys to field names.
    * @task customfield
    */
   private function getCustomFieldNameMap() {
     $fields = $this->getCustomFields();
     if (!$fields) {
       return array();
     }
 
     $map = array();
     foreach ($fields->getFields() as $field) {
       $key = $field->getFieldKey();
       $name = $field->getHeraldFieldName();
       $map[$this->getHeraldKeyFromCustomKey($key)] = $name;
     }
 
     return $map;
   }
 
 
   /**
    * Get the value for a custom field.
    *
    * @param string Herald field key.
    * @return wild Custom field value.
    * @task customfield
    */
   private function getCustomFieldValue($field_key) {
     $field = $this->getCustomField($field_key);
     if (!$field) {
       return null;
     }
 
     return $field->getHeraldFieldValue();
   }
 
 
   /**
    * Get the Herald conditions for a custom field.
    *
    * @param string Herald field key.
    * @return list<const> List of Herald conditions.
    * @task customfield
    */
   private function getCustomFieldConditions($field_key) {
     $field = $this->getCustomField($field_key);
     if (!$field) {
       return array(
         self::CONDITION_NEVER,
       );
     }
 
     return $field->getHeraldFieldConditions();
   }
 
 
   /**
    * Get the Herald value type for a custom field and condition.
    *
    * @param string Herald field key.
    * @param const Herald condition constant.
    * @return const|null Herald value type constant, or null to use the default.
    * @task customfield
    */
   private function getCustomFieldValueTypeForFieldAndCondition(
     $field_key,
     $condition) {
 
     $field = $this->getCustomField($field_key);
     if (!$field) {
       return self::VALUE_NONE;
     }
 
     return $field->getHeraldFieldValueType($condition);
   }
 
 
 /* -(  Applying Effects  )--------------------------------------------------- */
 
 
   /**
    * @task apply
    */
   protected function applyStandardEffect(HeraldEffect $effect) {
     $action = $effect->getAction();
 
     $rule_type = $effect->getRule()->getRuleType();
     $supported = $this->getActions($rule_type);
     $supported = array_fuse($supported);
     if (empty($supported[$action])) {
       return new HeraldApplyTranscript(
         $effect,
         false,
         pht(
           'Adapter "%s" does not support action "%s" for rule type "%s".',
           get_class($this),
           $action,
           $rule_type));
     }
 
     switch ($action) {
       case self::ACTION_ADD_PROJECTS:
       case self::ACTION_REMOVE_PROJECTS:
         return $this->applyProjectsEffect($effect);
       case self::ACTION_ADD_CC:
       case self::ACTION_REMOVE_CC:
         return $this->applySubscribersEffect($effect);
       case self::ACTION_FLAG:
         return $this->applyFlagEffect($effect);
       case self::ACTION_EMAIL:
         return $this->applyEmailEffect($effect);
       case self::ACTION_NOTHING:
         return $this->applyNothingEffect($effect);
       default:
         break;
     }
 
     $result = $this->handleCustomHeraldEffect($effect);
 
     if (!$result) {
       return new HeraldApplyTranscript(
         $effect,
         false,
         pht(
           'No custom action exists to handle rule action "%s".',
           $action));
     }
 
     return $result;
   }
 
   private function applyNothingEffect(HeraldEffect $effect) {
     return new HeraldApplyTranscript(
       $effect,
       true,
       pht('Did nothing.'));
   }
 
   /**
    * @task apply
    */
   private function applyProjectsEffect(HeraldEffect $effect) {
 
     if ($effect->getAction() == self::ACTION_ADD_PROJECTS) {
       $kind = '+';
     } else {
       $kind = '-';
     }
 
     $project_type = PhabricatorProjectObjectHasProjectEdgeType::EDGECONST;
     $project_phids = $effect->getTarget();
     $xaction = $this->newTransaction()
       ->setTransactionType(PhabricatorTransactions::TYPE_EDGE)
       ->setMetadataValue('edge:type', $project_type)
       ->setNewValue(
         array(
           $kind => array_fuse($project_phids),
         ));
 
     $this->queueTransaction($xaction);
 
     return new HeraldApplyTranscript(
       $effect,
       true,
       pht('Added projects.'));
   }
 
   /**
    * @task apply
    */
   private function applySubscribersEffect(HeraldEffect $effect) {
     if ($effect->getAction() == self::ACTION_ADD_CC) {
       $kind = '+';
       $is_add = true;
     } else {
       $kind = '-';
       $is_add = false;
     }
 
     $subscriber_phids = array_fuse($effect->getTarget());
     if (!$subscriber_phids) {
       return new HeraldApplyTranscript(
         $effect,
         false,
         pht('This action lists no users or objects to affect.'));
     }
 
     // The "Add Subscribers" rule only adds subscribers who haven't previously
     // unsubscribed from the object explicitly. Filter these subscribers out
     // before continuing.
     $unsubscribed = array();
     if ($is_add) {
       if ($this->unsubscribedPHIDs === null) {
         $this->unsubscribedPHIDs = PhabricatorEdgeQuery::loadDestinationPHIDs(
           $this->getObject()->getPHID(),
           PhabricatorObjectHasUnsubscriberEdgeType::EDGECONST);
       }
 
       foreach ($this->unsubscribedPHIDs as $phid) {
         if (isset($subscriber_phids[$phid])) {
           $unsubscribed[$phid] = $phid;
           unset($subscriber_phids[$phid]);
         }
       }
     }
 
     if (!$subscriber_phids) {
       return new HeraldApplyTranscript(
         $effect,
         false,
         pht('All targets have previously unsubscribed explicitly.'));
     }
 
     // Filter out PHIDs which aren't valid subscribers. Lower levels of the
     // stack will fail loudly if we try to add subscribers with invalid PHIDs
     // or unknown PHID types, so drop them here.
     $invalid = array();
     foreach ($subscriber_phids as $phid) {
       $type = phid_get_type($phid);
       switch ($type) {
         case PhabricatorPeopleUserPHIDType::TYPECONST:
         case PhabricatorProjectProjectPHIDType::TYPECONST:
           break;
         default:
           $invalid[$phid] = $phid;
           unset($subscriber_phids[$phid]);
           break;
       }
     }
 
     if (!$subscriber_phids) {
       return new HeraldApplyTranscript(
         $effect,
         false,
         pht('All targets are invalid as subscribers.'));
     }
 
     $xaction = $this->newTransaction()
       ->setTransactionType(PhabricatorTransactions::TYPE_SUBSCRIBERS)
       ->setNewValue(
         array(
           $kind => $subscriber_phids,
         ));
 
     $this->queueTransaction($xaction);
 
     // TODO: We could be more detailed about this, but doing it meaningfully
     // probably requires substantial changes to how transactions are rendered
     // first.
     if ($is_add) {
       $message = pht('Subscribed targets.');
     } else {
       $message = pht('Unsubscribed targets.');
     }
 
     return new HeraldApplyTranscript($effect, true, $message);
   }
 
 
   /**
    * @task apply
    */
   private function applyFlagEffect(HeraldEffect $effect) {
     $phid = $this->getPHID();
     $color = $effect->getTarget();
 
     $rule = $effect->getRule();
     $user = $rule->getAuthor();
 
     $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.'));
   }
 
 
   /**
    * @task apply
    */
   private function applyEmailEffect(HeraldEffect $effect) {
     foreach ($effect->getTarget() as $phid) {
       $this->emailPHIDs[$phid] = $phid;
 
       // If this is a personal rule, we'll force delivery of a real email. This
       // effect is stronger than notification preferences, so you get an actual
       // email even if your preferences are set to "Notify" or "Ignore".
       $rule = $effect->getRule();
       if ($rule->isPersonalRule()) {
         $this->forcedEmailPHIDs[$phid] = $phid;
       }
     }
 
     return new HeraldApplyTranscript(
       $effect,
       true,
       pht('Added mailable to mail targets.'));
   }
 
 
 }
diff --git a/src/applications/herald/adapter/HeraldDifferentialRevisionAdapter.php b/src/applications/herald/adapter/HeraldDifferentialRevisionAdapter.php
index afd328b34..a73125487 100644
--- a/src/applications/herald/adapter/HeraldDifferentialRevisionAdapter.php
+++ b/src/applications/herald/adapter/HeraldDifferentialRevisionAdapter.php
@@ -1,321 +1,322 @@
 <?php
 
 final class HeraldDifferentialRevisionAdapter
   extends HeraldDifferentialAdapter {
 
+  protected $diff;
   protected $revision;
 
   protected $explicitReviewers;
   protected $addReviewerPHIDs = array();
   protected $blockingReviewerPHIDs = array();
   protected $buildPlans = array();
   protected $requiredSignatureDocumentPHIDs = array();
 
   protected $affectedPackages;
   protected $changesets;
   private $haveHunks;
 
   public function getAdapterApplicationClass() {
     return 'PhabricatorDifferentialApplication';
   }
 
   protected function newObject() {
     return new DifferentialRevision();
   }
 
   public function getObject() {
     return $this->revision;
   }
 
   public function getDiff() {
     return $this->diff;
   }
 
   public function getAdapterContentType() {
     return 'differential';
   }
 
   public function getAdapterContentName() {
     return pht('Differential Revisions');
   }
 
   public function getAdapterContentDescription() {
     return pht(
       "React to revisions being created or updated.\n".
       "Revision rules can send email, flag revisions, add reviewers, ".
       "and run build plans.");
   }
 
   public function supportsRuleType($rule_type) {
     switch ($rule_type) {
       case HeraldRuleTypeConfig::RULE_TYPE_GLOBAL:
       case HeraldRuleTypeConfig::RULE_TYPE_PERSONAL:
         return true;
       case HeraldRuleTypeConfig::RULE_TYPE_OBJECT:
       default:
         return false;
     }
   }
 
   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_REPOSITORY_PROJECTS,
         self::FIELD_DIFF_FILE,
         self::FIELD_DIFF_CONTENT,
         self::FIELD_DIFF_ADDED_CONTENT,
         self::FIELD_DIFF_REMOVED_CONTENT,
         self::FIELD_AFFECTED_PACKAGE,
         self::FIELD_AFFECTED_PACKAGE_OWNER,
         self::FIELD_IS_NEW_OBJECT,
       ),
       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 setExplicitReviewers($explicit_reviewers) {
     $this->explicitReviewers = $explicit_reviewers;
     return $this;
   }
 
   public function getReviewersAddedByHerald() {
     return $this->addReviewerPHIDs;
   }
 
   public function getBlockingReviewersAddedByHerald() {
     return $this->blockingReviewerPHIDs;
   }
 
   public function getRequiredSignatureDocumentPHIDs() {
     return $this->requiredSignatureDocumentPHIDs;
   }
 
   public function getBuildPlans() {
     return $this->buildPlans;
   }
 
   public function getPHID() {
     return $this->revision->getPHID();
   }
 
   public function getHeraldName() {
     return $this->revision->getTitle();
   }
 
   protected function loadChangesets() {
     if ($this->changesets === null) {
       $this->changesets = $this->diff->loadChangesets();
     }
     return $this->changesets;
   }
 
   protected function loadChangesetsWithHunks() {
     $changesets = $this->loadChangesets();
 
     if ($changesets && !$this->haveHunks) {
       $this->haveHunks = true;
 
       id(new DifferentialHunkQuery())
         ->setViewer(PhabricatorUser::getOmnipotentUser())
         ->withChangesets($changesets)
         ->needAttachToChangesets(true)
         ->execute();
     }
 
     return $changesets;
   }
 
   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_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_REPOSITORY_PROJECTS:
         $repository = $this->loadRepository();
         if (!$repository) {
           return array();
         }
         return $repository->getProjectPHIDs();
       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_merge(
           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_REQUIRE_SIGNATURE,
             self::ACTION_NOTHING,
           ),
           parent::getActions($rule_type));
       case HeraldRuleTypeConfig::RULE_TYPE_PERSONAL:
         return array_merge(
           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,
           ),
           parent::getActions($rule_type));
     }
   }
 
   public function applyHeraldEffects(array $effects) {
     assert_instances_of($effects, 'HeraldEffect');
 
     $result = array();
 
     foreach ($effects as $effect) {
       $action = $effect->getAction();
       switch ($action) {
         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;
         case self::ACTION_REQUIRE_SIGNATURE:
           foreach ($effect->getTarget() as $phid) {
             $this->requiredSignatureDocumentPHIDs[] = $phid;
           }
           $result[] = new HeraldApplyTranscript(
             $effect,
             true,
             pht('Required signatures.'));
           break;
         default:
           $result[] = $this->applyStandardEffect($effect);
           break;
       }
     }
     return $result;
   }
 
 }
diff --git a/src/applications/herald/adapter/HeraldManiphestTaskAdapter.php b/src/applications/herald/adapter/HeraldManiphestTaskAdapter.php
index a84130095..906981af3 100644
--- a/src/applications/herald/adapter/HeraldManiphestTaskAdapter.php
+++ b/src/applications/herald/adapter/HeraldManiphestTaskAdapter.php
@@ -1,161 +1,162 @@
 <?php
 
 final class HeraldManiphestTaskAdapter extends HeraldAdapter {
 
   private $task;
   private $assignPHID;
 
   protected function newObject() {
     return new ManiphestTask();
   }
 
   public function getAdapterApplicationClass() {
     return 'PhabricatorManiphestApplication';
   }
 
   public function getAdapterContentDescription() {
     return pht('React to tasks being created or updated.');
   }
 
   public function getRepetitionOptions() {
     return array(
       HeraldRepetitionPolicyConfig::EVERY,
       HeraldRepetitionPolicyConfig::FIRST,
     );
   }
 
   public function supportsRuleType($rule_type) {
     switch ($rule_type) {
       case HeraldRuleTypeConfig::RULE_TYPE_GLOBAL:
       case HeraldRuleTypeConfig::RULE_TYPE_PERSONAL:
         return true;
       case HeraldRuleTypeConfig::RULE_TYPE_OBJECT:
       default:
         return false;
     }
   }
 
   public function setTask(ManiphestTask $task) {
     $this->task = $task;
     return $this;
   }
   public function getTask() {
     return $this->task;
   }
 
   public function getObject() {
     return $this->task;
   }
 
   public function setAssignPHID($assign_phid) {
     $this->assignPHID = $assign_phid;
     return $this;
   }
   public function getAssignPHID() {
     return $this->assignPHID;
   }
 
   public function getAdapterContentName() {
     return pht('Maniphest Tasks');
   }
 
   public function getFields() {
     return array_merge(
       array(
         self::FIELD_TITLE,
         self::FIELD_BODY,
         self::FIELD_AUTHOR,
         self::FIELD_ASSIGNEE,
         self::FIELD_CC,
         self::FIELD_CONTENT_SOURCE,
         self::FIELD_PROJECTS,
         self::FIELD_TASK_PRIORITY,
         self::FIELD_TASK_STATUS,
         self::FIELD_IS_NEW_OBJECT,
         self::FIELD_APPLICATION_EMAIL,
+        self::FIELD_SPACE,
       ),
       parent::getFields());
   }
 
   public function getActions($rule_type) {
     switch ($rule_type) {
       case HeraldRuleTypeConfig::RULE_TYPE_GLOBAL:
         return array_merge(
           array(
             self::ACTION_ADD_CC,
             self::ACTION_REMOVE_CC,
             self::ACTION_EMAIL,
             self::ACTION_ASSIGN_TASK,
             self::ACTION_NOTHING,
           ),
           parent::getActions($rule_type));
       case HeraldRuleTypeConfig::RULE_TYPE_PERSONAL:
         return array_merge(
           array(
             self::ACTION_ADD_CC,
             self::ACTION_REMOVE_CC,
             self::ACTION_EMAIL,
             self::ACTION_FLAG,
             self::ACTION_ASSIGN_TASK,
             self::ACTION_NOTHING,
           ),
           parent::getActions($rule_type));
     }
   }
 
   public function getPHID() {
     return $this->getTask()->getPHID();
   }
 
   public function getHeraldName() {
     return 'T'.$this->getTask()->getID();
   }
 
   public function getHeraldField($field) {
     switch ($field) {
       case self::FIELD_TITLE:
         return $this->getTask()->getTitle();
       case self::FIELD_BODY:
         return $this->getTask()->getDescription();
       case self::FIELD_AUTHOR:
         return $this->getTask()->getAuthorPHID();
       case self::FIELD_ASSIGNEE:
         return $this->getTask()->getOwnerPHID();
       case self::FIELD_PROJECTS:
         return PhabricatorEdgeQuery::loadDestinationPHIDs(
           $this->getTask()->getPHID(),
           PhabricatorProjectObjectHasProjectEdgeType::EDGECONST);
       case self::FIELD_TASK_PRIORITY:
         return $this->getTask()->getPriority();
       case self::FIELD_TASK_STATUS:
         return $this->getTask()->getStatus();
     }
 
     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_ASSIGN_TASK:
           $target_array = $effect->getTarget();
           $assign_phid = reset($target_array);
           $this->setAssignPHID($assign_phid);
           $result[] = new HeraldApplyTranscript(
             $effect,
             true,
             pht('Assigned task.'));
           break;
         default:
           $result[] = $this->applyStandardEffect($effect);
           break;
       }
     }
     return $result;
   }
 
 }
diff --git a/src/applications/herald/adapter/HeraldPholioMockAdapter.php b/src/applications/herald/adapter/HeraldPholioMockAdapter.php
index e4aab69e9..5c9c78c0a 100644
--- a/src/applications/herald/adapter/HeraldPholioMockAdapter.php
+++ b/src/applications/herald/adapter/HeraldPholioMockAdapter.php
@@ -1,121 +1,122 @@
 <?php
 
 final class HeraldPholioMockAdapter extends HeraldAdapter {
 
   private $mock;
 
   public function getAdapterApplicationClass() {
     return 'PhabricatorPholioApplication';
   }
 
   public function getAdapterContentDescription() {
     return pht('React to mocks being created or updated.');
   }
 
   protected function newObject() {
     return new PholioMock();
   }
 
   public function getObject() {
     return $this->mock;
   }
 
   public function setMock(PholioMock $mock) {
     $this->mock = $mock;
     return $this;
   }
   public function getMock() {
     return $this->mock;
   }
 
   public function getAdapterContentName() {
     return pht('Pholio Mocks');
   }
 
   public function supportsRuleType($rule_type) {
     switch ($rule_type) {
       case HeraldRuleTypeConfig::RULE_TYPE_GLOBAL:
       case HeraldRuleTypeConfig::RULE_TYPE_PERSONAL:
         return true;
       case HeraldRuleTypeConfig::RULE_TYPE_OBJECT:
       default:
         return false;
     }
   }
 
   public function getFields() {
     return array_merge(
       array(
         self::FIELD_TITLE,
         self::FIELD_BODY,
         self::FIELD_AUTHOR,
         self::FIELD_CC,
         self::FIELD_PROJECTS,
         self::FIELD_IS_NEW_OBJECT,
+        self::FIELD_SPACE,
       ),
       parent::getFields());
   }
 
   public function getActions($rule_type) {
     switch ($rule_type) {
       case HeraldRuleTypeConfig::RULE_TYPE_GLOBAL:
         return array_merge(
           array(
             self::ACTION_ADD_CC,
             self::ACTION_REMOVE_CC,
             self::ACTION_NOTHING,
           ),
           parent::getActions($rule_type));
       case HeraldRuleTypeConfig::RULE_TYPE_PERSONAL:
         return array_merge(
           array(
             self::ACTION_ADD_CC,
             self::ACTION_REMOVE_CC,
             self::ACTION_FLAG,
             self::ACTION_NOTHING,
           ),
           parent::getActions($rule_type));
     }
   }
 
   public function getPHID() {
     return $this->getMock()->getPHID();
   }
 
   public function getHeraldName() {
     return 'M'.$this->getMock()->getID();
   }
 
   public function getHeraldField($field) {
     switch ($field) {
       case self::FIELD_TITLE:
         return $this->getMock()->getName();
       case self::FIELD_BODY:
         return $this->getMock()->getDescription();
       case self::FIELD_AUTHOR:
         return $this->getMock()->getAuthorPHID();
       case self::FIELD_PROJECTS:
         return PhabricatorEdgeQuery::loadDestinationPHIDs(
           $this->getMock()->getPHID(),
           PhabricatorProjectObjectHasProjectEdgeType::EDGECONST);
     }
 
     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) {
         default:
           $result[] = $this->applyStandardEffect($effect);
           break;
       }
     }
     return $result;
   }
 
 }
diff --git a/src/applications/herald/config/HeraldRepetitionPolicyConfig.php b/src/applications/herald/config/HeraldRepetitionPolicyConfig.php
index aa64a0ecc..40e84f0ae 100644
--- a/src/applications/herald/config/HeraldRepetitionPolicyConfig.php
+++ b/src/applications/herald/config/HeraldRepetitionPolicyConfig.php
@@ -1,28 +1,28 @@
 <?php
 
-final class HeraldRepetitionPolicyConfig {
+final class HeraldRepetitionPolicyConfig extends Phobject {
 
   const FIRST   = 'first';  // only execute the first time (no repeating)
   const EVERY   = 'every';  // repeat every time
 
   private static $policyIntMap = array(
     self::FIRST   => 0,
     self::EVERY   => 1,
   );
 
   public static function getMap() {
     return array(
       self::EVERY   => pht('every time'),
       self::FIRST   => pht('only the first time'),
     );
   }
 
   public static function toInt($str) {
     return idx(self::$policyIntMap, $str, self::$policyIntMap[self::EVERY]);
   }
 
   public static function toString($int) {
     return idx(array_flip(self::$policyIntMap), $int, self::EVERY);
   }
 
 }
diff --git a/src/applications/herald/config/HeraldRuleTypeConfig.php b/src/applications/herald/config/HeraldRuleTypeConfig.php
index d55c435f4..36b827b3e 100644
--- a/src/applications/herald/config/HeraldRuleTypeConfig.php
+++ b/src/applications/herald/config/HeraldRuleTypeConfig.php
@@ -1,17 +1,17 @@
 <?php
 
-final class HeraldRuleTypeConfig {
+final class HeraldRuleTypeConfig extends Phobject {
 
   const RULE_TYPE_GLOBAL = 'global';
   const RULE_TYPE_OBJECT = 'object';
   const RULE_TYPE_PERSONAL = 'personal';
 
   public static function getRuleTypeMap() {
     $map = array(
       self::RULE_TYPE_PERSONAL => pht('Personal'),
       self::RULE_TYPE_OBJECT => pht('Object'),
       self::RULE_TYPE_GLOBAL => pht('Global'),
     );
     return $map;
   }
 }
diff --git a/src/applications/herald/controller/HeraldRuleController.php b/src/applications/herald/controller/HeraldRuleController.php
index a2fbe7a10..a956063fd 100644
--- a/src/applications/herald/controller/HeraldRuleController.php
+++ b/src/applications/herald/controller/HeraldRuleController.php
@@ -1,705 +1,706 @@
 <?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);
 
       $adapter = HeraldAdapter::getAdapterForContentType(
         $rule->getContentType());
 
       if (!$adapter->supportsRuleType($rule->getRuleType())) {
         throw new Exception(
           pht(
             "This rule's content type does not support the selected rule ".
             "type."));
       }
 
       if ($rule->isObjectRule()) {
         $rule->setTriggerObjectPHID($request->getStr('targetPHID'));
         $object = id(new PhabricatorObjectQuery())
           ->setViewer($user)
           ->withPHIDs(array($rule->getTriggerObjectPHID()))
           ->requireCapabilities(
             array(
               PhabricatorPolicyCapability::CAN_VIEW,
               PhabricatorPolicyCapability::CAN_EDIT,
             ))
           ->executeOne();
         if (!$object) {
           throw new Exception(
             pht('No valid object provided for object rule!'));
         }
 
         if (!$adapter->canTriggerOnObject($object)) {
           throw new Exception(
             pht('Object is of wrong type for adapter!'));
         }
       }
 
       $cancel_uri = $this->getApplicationURI();
     }
 
     if ($rule->isGlobalRule()) {
       $this->requireApplicationCapability(
         HeraldManageGlobalRulesCapability::CAPABILITY);
     }
 
     $adapter = HeraldAdapter::getAdapterForContentType($rule->getContentType());
 
     $local_version = id(new HeraldRule())->getConfigVersion();
     if ($rule->getConfigVersion() > $local_version) {
       throw new Exception(
         pht(
           '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);
       }
     }
 
     $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()));
 
     $trigger_object_control = false;
     if ($rule->isObjectRule()) {
       $trigger_object_control = id(new AphrontFormStaticControl())
         ->setValue(
           pht(
             'This rule triggers for %s.',
             $handles[$rule->getTriggerObjectPHID()]->renderLink()));
     }
 
 
     $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($trigger_object_control)
       ->appendChild(
         id(new PHUIFormInsetView())
           ->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 PHUIFormInsetView())
           ->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)
       ->setFormErrors($errors)
       ->setForm($form);
 
     $crumbs = $this
       ->buildApplicationCrumbs()
       ->addTextCrumb($title);
 
     return $this->buildApplicationPage(
       array(
         $crumbs,
         $form_box,
       ),
       array(
         'title' => pht('Edit Rule'),
       ));
   }
 
   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 = null;
     try {
       $data = phutil_json_decode($request->getStr('rule'));
     } catch (PhutilJSONParserException $ex) {
       throw new PhutilProxyException(
         pht('Failed to decode rule data.'),
         $ex);
     }
 
     if (!is_array($data) ||
         !$data['conditions'] ||
         !$data['actions']) {
       throw new Exception(pht('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->getMessage();
       }
 
       $actions[] = $obj;
     }
 
     $rule->attachConditions($conditions);
     $rule->attachActions($actions);
 
     if (!$errors) {
       $edit_action = $rule->getID() ? 'edit' : 'create';
 
       $rule->openTransaction();
         $rule->save();
         $rule->saveConditions($conditions);
         $rule->saveActions($actions);
       $rule->saveTransaction();
     }
 
     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();
         switch ($condition->getFieldName()) {
           case HeraldAdapter::FIELD_TASK_PRIORITY:
             $value_map = array();
             $priority_map = ManiphestTaskPriority::getTaskPriorityMap();
             foreach ($value as $priority) {
               $value_map[$priority] = idx($priority_map, $priority);
             }
             $value = $value_map;
             break;
           case HeraldAdapter::FIELD_TASK_STATUS:
             $value_map = array();
             $status_map = ManiphestTaskStatus::getTaskStatusMap();
             foreach ($value as $status) {
               $value_map[$status] = idx($status_map, $status);
             }
             $value = $value_map;
             break;
           default:
             if (is_array($value)) {
               $value_map = array();
               foreach ($value as $k => $fbid) {
                 $value_map[$fbid] = $handles[$fbid]->getName();
               }
               $value = $value_map;
             }
             break;
         }
         $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:
           case HeraldAdapter::ACTION_BLOCK:
             $current_value = $action->getTarget();
             break;
           default:
             if (is_array($action->getTarget())) {
               $target_map = array();
               foreach ((array)$action->getTarget() as $fbid) {
                 $target_map[$fbid] = $handles[$fbid]->getName();
               }
               $current_value = $target_map;
             } else {
               $current_value = $action->getTarget();
             }
             break;
         }
 
         $serial_actions[] = array(
           $action->getAction(),
           $current_value,
         );
       }
     }
 
     $all_rules = $this->loadRulesThisRuleMayDependUpon($rule);
     $all_rules = mpull($all_rules, 'getName', 'getPHID');
     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);
 
     // Populate any fields which exist in the rule but which we don't know the
     // names of, so that saving a rule without touching anything doesn't change
     // it.
     foreach ($rule->getConditions() as $condition) {
       $field_name = $condition->getFieldName();
 
       if (empty($field_map[$field_name])) {
         $field_map[$field_name] = pht('<Unknown Field "%s">', $field_name);
       }
     }
 
     $actions = $adapter->getActions($rule->getRuleType());
     $action_map = array_select_keys($all_actions, $actions);
 
     // Populate any actions which exist in the rule but which we don't know the
     // names of, so that saving a rule without touching anything doesn't change
     // it.
     foreach ($rule->getActions() as $action) {
       $action_name = $action->getAction();
 
       if (empty($action_map[$action_name])) {
         $action_map[$action_name] = pht('<Unknown Action "%s">', $action_name);
       }
     }
 
 
     $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) {
       try {
         $field_conditions = $adapter->getConditionsForField($field);
       } catch (Exception $ex) {
         $field_conditions = array(HeraldAdapter::CONDITION_UNCONDITIONALLY);
       }
       $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) {
       try {
         $action_value = $adapter->getValueTypeForAction(
           $action,
          $rule->getRuleType());
       } catch (Exception $ex) {
         $action_value = array(HeraldAdapter::VALUE_NONE);
       }
 
       $config_info['targets'][$action] = $action_value;
     }
 
     $changeflag_options =
       PhabricatorRepositoryPushLog::getHeraldChangeFlagConditionOptions();
     Javelin::initBehavior(
       'herald-rule-editor',
       array(
         'root' => 'herald-rule-edit-form',
         'conditions' => (object)$serial_conditions,
         'actions' => (object)$serial_actions,
         'select' => array(
           HeraldAdapter::VALUE_CONTENT_SOURCE => array(
             'options' => PhabricatorContentSource::getSourceNameMap(),
             'default' => PhabricatorContentSource::SOURCE_WEB,
           ),
           HeraldAdapter::VALUE_FLAG_COLOR => array(
             'options' => PhabricatorFlagColor::getColorNameMap(),
             'default' => PhabricatorFlagColor::COLOR_BLUE,
           ),
           HeraldPreCommitRefAdapter::VALUE_REF_TYPE => array(
             'options' => array(
               PhabricatorRepositoryPushLog::REFTYPE_BRANCH
                 => pht('branch (git/hg)'),
               PhabricatorRepositoryPushLog::REFTYPE_TAG
                 => pht('tag (git)'),
               PhabricatorRepositoryPushLog::REFTYPE_BOOKMARK
                 => pht('bookmark (hg)'),
             ),
             'default' => PhabricatorRepositoryPushLog::REFTYPE_BRANCH,
           ),
           HeraldPreCommitRefAdapter::VALUE_REF_CHANGE => array(
             'options' => $changeflag_options,
             'default' => PhabricatorRepositoryPushLog::CHANGEFLAG_ADD,
           ),
         ),
         'template' => $this->buildTokenizerTemplates($handles) + array(
           'rules' => $all_rules,
         ),
         '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();
 
     if ($rule->isObjectRule()) {
       $phids[] = $rule->getTriggerObjectPHID();
     }
 
     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(array $handles) {
     $template = new AphrontTokenizerTemplateView();
     $template = $template->render();
 
     $sources = array(
       'repository' => new DiffusionRepositoryDatasource(),
       'legaldocuments' => new LegalpadDocumentDatasource(),
       'taskpriority' => new ManiphestTaskPriorityDatasource(),
       'taskstatus' => new ManiphestTaskStatusDatasource(),
       'buildplan' => new HarbormasterBuildPlanDatasource(),
       'package' => new PhabricatorOwnersPackageDatasource(),
       'project' => new PhabricatorProjectDatasource(),
       'user' => new PhabricatorPeopleDatasource(),
       'email' => new PhabricatorMetaMTAMailableDatasource(),
       'userorproject' => new PhabricatorProjectOrUserDatasource(),
       'applicationemail' => new PhabricatorMetaMTAApplicationEmailDatasource(),
+      'space' => new PhabricatorSpacesNamespaceDatasource(),
     );
 
     foreach ($sources as $key => $source) {
       $source->setViewer($this->getViewer());
 
       $sources[$key] = array(
         'uri' => $source->getDatasourceURI(),
         'placeholder' => $source->getPlaceholderText(),
         'browseURI' => $source->getBrowseURI(),
       );
     }
 
     return array(
       'source' => $sources,
       'username' => $this->getRequest()->getUser()->getUserName(),
       'icons' => mpull($handles, 'getTypeIcon', 'getPHID'),
       '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->isObjectRule()) {
       // Object rules may depend on other rules for the same object.
       $all_rules += id(new HeraldRuleQuery())
         ->setViewer($viewer)
         ->withRuleTypes(array(HeraldRuleTypeConfig::RULE_TYPE_OBJECT))
         ->withContentTypes(array($rule->getContentType()))
         ->withTriggerObjectPHIDs(array($rule->getTriggerObjectPHID()))
         ->execute();
     }
 
     if ($rule->isPersonalRule()) {
       // 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();
     }
 
     // mark disabled rules as disabled since they are not useful as such;
     // don't filter though to keep edit cases sane / expected
     foreach ($all_rules as $current_rule) {
       if ($current_rule->getIsDisabled()) {
         $current_rule->makeEphemeral();
         $current_rule->setName($rule->getName().' '.pht('(Disabled)'));
       }
     }
 
     // A rule can not depend upon itself.
     unset($all_rules[$rule->getID()]);
 
     return $all_rules;
   }
 
 }
diff --git a/src/applications/herald/engine/HeraldEffect.php b/src/applications/herald/engine/HeraldEffect.php
index d9a536be3..52d86cf5d 100644
--- a/src/applications/herald/engine/HeraldEffect.php
+++ b/src/applications/herald/engine/HeraldEffect.php
@@ -1,56 +1,56 @@
 <?php
 
-final class HeraldEffect {
+final class HeraldEffect extends Phobject {
 
   private $objectPHID;
   private $action;
   private $target;
   private $rule;
   private $reason;
 
   public function setObjectPHID($object_phid) {
     $this->objectPHID = $object_phid;
     return $this;
   }
 
   public function getObjectPHID() {
     return $this->objectPHID;
   }
 
   public function setAction($action) {
     $this->action = $action;
     return $this;
   }
 
   public function getAction() {
     return $this->action;
   }
 
   public function setTarget($target) {
     $this->target = $target;
     return $this;
   }
 
   public function getTarget() {
     return $this->target;
   }
 
   public function setRule(HeraldRule $rule) {
     $this->rule = $rule;
     return $this;
   }
 
   public function getRule() {
     return $this->rule;
   }
 
   public function setReason($reason) {
     $this->reason = $reason;
     return $this;
   }
 
   public function getReason() {
     return $this->reason;
   }
 
 }
diff --git a/src/applications/herald/engine/HeraldEngine.php b/src/applications/herald/engine/HeraldEngine.php
index 4e986987d..00c063131 100644
--- a/src/applications/herald/engine/HeraldEngine.php
+++ b/src/applications/herald/engine/HeraldEngine.php
@@ -1,451 +1,452 @@
 <?php
 
-final class HeraldEngine {
+final class HeraldEngine extends Phobject {
 
   protected $rules = array();
   protected $results = array();
   protected $stack = array();
-  protected $activeRule = null;
+  protected $activeRule;
+  protected $transcript;
 
   protected $fieldCache = array();
-  protected $object = null;
+  protected $object;
   private $dryRun;
 
   public function setDryRun($dry_run) {
     $this->dryRun = $dry_run;
     return $this;
   }
 
   public function getDryRun() {
     return $this->dryRun;
   }
 
   public function getRule($phid) {
     return idx($this->rules, $phid);
   }
 
   public function loadRulesForAdapter(HeraldAdapter $adapter) {
     return id(new HeraldRuleQuery())
       ->setViewer(PhabricatorUser::getOmnipotentUser())
       ->withDisabled(false)
       ->withContentTypes(array($adapter->getAdapterContentType()))
       ->needConditionsAndActions(true)
       ->needAppliedToPHIDs(array($adapter->getPHID()))
       ->needValidateAuthors(true)
       ->execute();
   }
 
   public static function loadAndApplyRules(HeraldAdapter $adapter) {
     $engine = new HeraldEngine();
 
     $rules = $engine->loadRulesForAdapter($adapter);
     $effects = $engine->applyRules($rules, $adapter);
     $engine->applyEffects($effects, $adapter, $rules);
 
     return $engine->getTranscript();
   }
 
   public function applyRules(array $rules, HeraldAdapter $object) {
     assert_instances_of($rules, 'HeraldRule');
     $t_start = microtime(true);
 
     // Rules execute in a well-defined order: sort them into execution order.
     $rules = msort($rules, 'getRuleExecutionOrderSortKey');
     $rules = mpull($rules, null, 'getPHID');
 
     $this->transcript = new HeraldTranscript();
     $this->transcript->setObjectPHID((string)$object->getPHID());
     $this->fieldCache = array();
     $this->results = array();
     $this->rules   = $rules;
     $this->object  = $object;
 
     $effects = array();
     foreach ($rules as $phid => $rule) {
       $this->stack = array();
 
       $policy_first = HeraldRepetitionPolicyConfig::FIRST;
       $policy_first_int = HeraldRepetitionPolicyConfig::toInt($policy_first);
       $is_first_only = ($rule->getRepetitionPolicy() == $policy_first_int);
 
       try {
         if (!$this->getDryRun() &&
             $is_first_only &&
             $rule->getRuleApplied($object->getPHID())) {
           // This is not a dry run, and this rule is only supposed to be
           // applied a single time, and it's already been applied...
           // That means automatic failure.
           $xscript = id(new HeraldRuleTranscript())
             ->setRuleID($rule->getID())
             ->setResult(false)
             ->setRuleName($rule->getName())
             ->setRuleOwner($rule->getAuthorPHID())
             ->setReason(
               pht(
                 'This rule is only supposed to be repeated a single time, '.
                 'and it has already been applied.'));
           $this->transcript->addRuleTranscript($xscript);
           $rule_matches = false;
         } else {
           $rule_matches = $this->doesRuleMatch($rule, $object);
         }
       } catch (HeraldRecursiveConditionsException $ex) {
         $names = array();
         foreach ($this->stack as $rule_id => $ignored) {
           $names[] = '"'.$rules[$rule_id]->getName().'"';
         }
         $names = implode(', ', $names);
         foreach ($this->stack as $rule_id => $ignored) {
           $xscript = new HeraldRuleTranscript();
           $xscript->setRuleID($rule_id);
           $xscript->setResult(false);
           $xscript->setReason(
             pht(
               "Rules %s are recursively dependent upon one another! ".
               "Don't do this! You have formed an unresolvable cycle in the ".
               "dependency graph!",
               $names));
           $xscript->setRuleName($rules[$rule_id]->getName());
           $xscript->setRuleOwner($rules[$rule_id]->getAuthorPHID());
           $this->transcript->addRuleTranscript($xscript);
         }
         $rule_matches = false;
       }
       $this->results[$phid] = $rule_matches;
 
       if ($rule_matches) {
         foreach ($this->getRuleEffects($rule, $object) as $effect) {
           $effects[] = $effect;
         }
       }
     }
 
     $object_transcript = new HeraldObjectTranscript();
     $object_transcript->setPHID($object->getPHID());
     $object_transcript->setName($object->getHeraldName());
     $object_transcript->setType($object->getAdapterContentType());
     $object_transcript->setFields($this->fieldCache);
 
     $this->transcript->setObjectTranscript($object_transcript);
 
     $t_end = microtime(true);
 
     $this->transcript->setDuration($t_end - $t_start);
 
     return $effects;
   }
 
   public function applyEffects(
     array $effects,
     HeraldAdapter $adapter,
     array $rules) {
     assert_instances_of($effects, 'HeraldEffect');
     assert_instances_of($rules, 'HeraldRule');
 
     $this->transcript->setDryRun((int)$this->getDryRun());
 
     if ($this->getDryRun()) {
       $xscripts = array();
       foreach ($effects as $effect) {
         $xscripts[] = new HeraldApplyTranscript(
           $effect,
           false,
           pht('This was a dry run, so no actions were actually taken.'));
       }
     } else {
       $xscripts = $adapter->applyHeraldEffects($effects);
     }
 
     assert_instances_of($xscripts, 'HeraldApplyTranscript');
     foreach ($xscripts as $apply_xscript) {
       $this->transcript->addApplyTranscript($apply_xscript);
     }
 
     // For dry runs, don't mark the rule as having applied to the object.
     if ($this->getDryRun()) {
       return;
     }
 
     $rules = mpull($rules, null, 'getID');
     $applied_ids = array();
     $first_policy = HeraldRepetitionPolicyConfig::toInt(
       HeraldRepetitionPolicyConfig::FIRST);
 
     // Mark all the rules that have had their effects applied as having been
     // executed for the current object.
     $rule_ids = mpull($xscripts, 'getRuleID');
 
     foreach ($rule_ids as $rule_id) {
       if (!$rule_id) {
         // Some apply transcripts are purely informational and not associated
         // with a rule, e.g. carryover emails from earlier revisions.
         continue;
       }
 
       $rule = idx($rules, $rule_id);
       if (!$rule) {
         continue;
       }
 
       if ($rule->getRepetitionPolicy() == $first_policy) {
         $applied_ids[] = $rule_id;
       }
     }
 
     if ($applied_ids) {
       $conn_w = id(new HeraldRule())->establishConnection('w');
       $sql = array();
       foreach ($applied_ids as $id) {
         $sql[] = qsprintf(
           $conn_w,
           '(%s, %d)',
           $adapter->getPHID(),
           $id);
       }
       queryfx(
         $conn_w,
         'INSERT IGNORE INTO %T (phid, ruleID) VALUES %Q',
         HeraldRule::TABLE_RULE_APPLIED,
         implode(', ', $sql));
     }
   }
 
   public function getTranscript() {
     $this->transcript->save();
     return $this->transcript;
   }
 
   public function doesRuleMatch(
     HeraldRule $rule,
     HeraldAdapter $object) {
 
     $phid = $rule->getPHID();
 
     if (isset($this->results[$phid])) {
       // If we've already evaluated this rule because another rule depends
       // on it, we don't need to reevaluate it.
       return $this->results[$phid];
     }
 
     if (isset($this->stack[$phid])) {
       // We've recursed, fail all of the rules on the stack. This happens when
       // there's a dependency cycle with "Rule conditions match for rule ..."
       // conditions.
       foreach ($this->stack as $rule_phid => $ignored) {
         $this->results[$rule_phid] = false;
       }
       throw new HeraldRecursiveConditionsException();
     }
 
     $this->stack[$phid] = true;
 
     $all = $rule->getMustMatchAll();
 
     $conditions = $rule->getConditions();
 
     $result = null;
 
     $local_version = id(new HeraldRule())->getConfigVersion();
     if ($rule->getConfigVersion() > $local_version) {
       $reason = pht(
         'Rule could not be processed, it was created with a newer version '.
         'of Herald.');
       $result = false;
     } else if (!$conditions) {
       $reason = pht(
         'Rule failed automatically because it has no conditions.');
       $result = false;
     } else if (!$rule->hasValidAuthor()) {
       $reason = pht(
         'Rule failed automatically because its owner is invalid '.
         'or disabled.');
       $result = false;
     } else if (!$this->canAuthorViewObject($rule, $object)) {
       $reason = pht(
         'Rule failed automatically because it is a personal rule and its '.
         'owner can not see the object.');
       $result = false;
     } else if (!$this->canRuleApplyToObject($rule, $object)) {
       $reason = pht(
         'Rule failed automatically because it is an object rule which is '.
         'not relevant for this object.');
       $result = false;
     } else {
       foreach ($conditions as $condition) {
         try {
           $object->getHeraldField($condition->getFieldName());
         } catch (Exception $ex) {
           $reason = pht(
             'Field "%s" does not exist!',
             $condition->getFieldName());
           $result = false;
           break;
         }
 
         $match = $this->doesConditionMatch($rule, $condition, $object);
 
         if (!$all && $match) {
           $reason = pht('Any condition matched.');
           $result = true;
           break;
         }
 
         if ($all && !$match) {
           $reason = pht('Not all conditions matched.');
           $result = false;
           break;
         }
       }
 
       if ($result === null) {
         if ($all) {
           $reason = pht('All conditions matched.');
           $result = true;
         } else {
           $reason = pht('No conditions matched.');
           $result = false;
         }
       }
     }
 
     $rule_transcript = new HeraldRuleTranscript();
     $rule_transcript->setRuleID($rule->getID());
     $rule_transcript->setResult($result);
     $rule_transcript->setReason($reason);
     $rule_transcript->setRuleName($rule->getName());
     $rule_transcript->setRuleOwner($rule->getAuthorPHID());
 
     $this->transcript->addRuleTranscript($rule_transcript);
 
     return $result;
   }
 
   protected function doesConditionMatch(
     HeraldRule $rule,
     HeraldCondition $condition,
     HeraldAdapter $object) {
 
     $object_value = $this->getConditionObjectValue($condition, $object);
     $test_value   = $condition->getValue();
 
     $cond = $condition->getFieldCondition();
 
     $transcript = new HeraldConditionTranscript();
     $transcript->setRuleID($rule->getID());
     $transcript->setConditionID($condition->getID());
     $transcript->setFieldName($condition->getFieldName());
     $transcript->setCondition($cond);
     $transcript->setTestValue($test_value);
 
     try {
       $result = $object->doesConditionMatch(
         $this,
         $rule,
         $condition,
         $object_value);
     } catch (HeraldInvalidConditionException $ex) {
       $result = false;
       $transcript->setNote($ex->getMessage());
     }
 
     $transcript->setResult($result);
 
     $this->transcript->addConditionTranscript($transcript);
 
     return $result;
   }
 
   protected function getConditionObjectValue(
     HeraldCondition $condition,
     HeraldAdapter $object) {
 
     $field = $condition->getFieldName();
 
     return $this->getObjectFieldValue($field);
   }
 
   public function getObjectFieldValue($field) {
     if (isset($this->fieldCache[$field])) {
       return $this->fieldCache[$field];
     }
 
     $result = $this->object->getHeraldField($field);
 
     $this->fieldCache[$field] = $result;
     return $result;
   }
 
   protected function getRuleEffects(
     HeraldRule $rule,
     HeraldAdapter $object) {
 
     $effects = array();
     foreach ($rule->getActions() as $action) {
       $effect = id(new HeraldEffect())
         ->setObjectPHID($object->getPHID())
         ->setAction($action->getAction())
         ->setTarget($action->getTarget())
         ->setRule($rule);
 
       $name = $rule->getName();
       $id = $rule->getID();
       $effect->setReason(
         pht(
           'Conditions were met for %s',
           "H{$id} {$name}"));
 
       $effects[] = $effect;
     }
     return $effects;
   }
 
   private function canAuthorViewObject(
     HeraldRule $rule,
     HeraldAdapter $adapter) {
 
     // Authorship is irrelevant for global rules and object rules.
     if ($rule->isGlobalRule() || $rule->isObjectRule()) {
       return true;
     }
 
     // The author must be able to create rules for the adapter's content type.
     // In particular, this means that the application must be installed and
     // accessible to the user. For example, if a user writes a Differential
     // rule and then loses access to Differential, this disables the rule.
     $enabled = HeraldAdapter::getEnabledAdapterMap($rule->getAuthor());
     if (empty($enabled[$adapter->getAdapterContentType()])) {
       return false;
     }
 
     // Finally, the author must be able to see the object itself. You can't
     // write a personal rule that CC's you on revisions you wouldn't otherwise
     // be able to see, for example.
     $object = $adapter->getObject();
     return PhabricatorPolicyFilter::hasCapability(
       $rule->getAuthor(),
       $object,
       PhabricatorPolicyCapability::CAN_VIEW);
   }
 
   private function canRuleApplyToObject(
     HeraldRule $rule,
     HeraldAdapter $adapter) {
 
     // Rules which are not object rules can apply to anything.
     if (!$rule->isObjectRule()) {
       return true;
     }
 
     $trigger_phid = $rule->getTriggerObjectPHID();
     $object_phids = $adapter->getTriggerObjectPHIDs();
 
     if ($object_phids) {
       if (in_array($trigger_phid, $object_phids)) {
         return true;
       }
     }
 
     return false;
   }
 
 }
diff --git a/src/applications/herald/extension/HeraldCustomAction.php b/src/applications/herald/extension/HeraldCustomAction.php
index 6d38494fc..2701a8ae6 100644
--- a/src/applications/herald/extension/HeraldCustomAction.php
+++ b/src/applications/herald/extension/HeraldCustomAction.php
@@ -1,20 +1,20 @@
 <?php
 
-abstract class HeraldCustomAction {
+abstract class HeraldCustomAction extends Phobject {
 
   abstract public function appliesToAdapter(HeraldAdapter $adapter);
 
   abstract public function appliesToRuleType($rule_type);
 
   abstract public function getActionKey();
 
   abstract public function getActionName();
 
   abstract public function getActionType();
 
   abstract public function applyEffect(
     HeraldAdapter $adapter,
     $object,
     HeraldEffect $effect);
 
 }
diff --git a/src/applications/herald/garbagecollector/HeraldTranscriptGarbageCollector.php b/src/applications/herald/garbagecollector/HeraldTranscriptGarbageCollector.php
index 18e3ce0b7..583778e53 100644
--- a/src/applications/herald/garbagecollector/HeraldTranscriptGarbageCollector.php
+++ b/src/applications/herald/garbagecollector/HeraldTranscriptGarbageCollector.php
@@ -1,31 +1,31 @@
 <?php
 
 final class HeraldTranscriptGarbageCollector
   extends PhabricatorGarbageCollector {
 
   public function collectGarbage() {
     $ttl = PhabricatorEnv::getEnvConfig('gcdaemon.ttl.herald-transcripts');
     if ($ttl <= 0) {
       return false;
     }
 
     $table = new HeraldTranscript();
     $conn_w = $table->establishConnection('w');
 
     queryfx(
       $conn_w,
       'UPDATE %T SET
           objectTranscript     = "",
           ruleTranscripts      = "",
           conditionTranscripts = "",
           applyTranscripts     = "",
           garbageCollected     = 1
-        WHERE garbageCollected = 0 AND `time` < %d
+        WHERE garbageCollected = 0 AND time < %d
         LIMIT 100',
       $table->getTableName(),
       time() - $ttl);
 
     return ($conn_w->getAffectedRows() == 100);
   }
 
 }
diff --git a/src/applications/herald/storage/transcript/HeraldConditionTranscript.php b/src/applications/herald/storage/transcript/HeraldConditionTranscript.php
index f78f31a55..36a534e6b 100644
--- a/src/applications/herald/storage/transcript/HeraldConditionTranscript.php
+++ b/src/applications/herald/storage/transcript/HeraldConditionTranscript.php
@@ -1,75 +1,75 @@
 <?php
 
-final class HeraldConditionTranscript {
+final class HeraldConditionTranscript extends Phobject {
 
   protected $ruleID;
   protected $conditionID;
   protected $fieldName;
   protected $condition;
   protected $testValue;
   protected $note;
   protected $result;
 
   public function setRuleID($rule_id) {
     $this->ruleID = $rule_id;
     return $this;
   }
 
   public function getRuleID() {
     return $this->ruleID;
   }
 
   public function setConditionID($condition_id) {
     $this->conditionID = $condition_id;
     return $this;
   }
 
   public function getConditionID() {
     return $this->conditionID;
   }
 
   public function setFieldName($field_name) {
     $this->fieldName = $field_name;
     return $this;
   }
 
   public function getFieldName() {
     return $this->fieldName;
   }
 
   public function setCondition($condition) {
     $this->condition = $condition;
     return $this;
   }
 
   public function getCondition() {
     return $this->condition;
   }
 
   public function setTestValue($test_value) {
     $this->testValue = $test_value;
     return $this;
   }
 
   public function getTestValue() {
     return $this->testValue;
   }
 
   public function setNote($note) {
     $this->note = $note;
     return $this;
   }
 
   public function getNote() {
     return $this->note;
   }
 
   public function setResult($result) {
     $this->result = $result;
     return $this;
   }
 
   public function getResult() {
     return $this->result;
   }
 }
diff --git a/src/applications/herald/storage/transcript/HeraldObjectTranscript.php b/src/applications/herald/storage/transcript/HeraldObjectTranscript.php
index 1edc24adc..e0b85162a 100644
--- a/src/applications/herald/storage/transcript/HeraldObjectTranscript.php
+++ b/src/applications/herald/storage/transcript/HeraldObjectTranscript.php
@@ -1,75 +1,75 @@
 <?php
 
-final class HeraldObjectTranscript {
+final class HeraldObjectTranscript extends Phobject {
 
   protected $phid;
   protected $type;
   protected $name;
   protected $fields;
 
   public function setPHID($phid) {
     $this->phid = $phid;
     return $this;
   }
 
   public function getPHID() {
     return $this->phid;
   }
 
   public function setType($type) {
     $this->type = $type;
     return $this;
   }
 
   public function getType() {
     return $this->type;
   }
 
   public function setName($name) {
     $this->name = $name;
     return $this;
   }
 
   public function getName() {
     return $this->name;
   }
 
   public function setFields(array $fields) {
     foreach ($fields as $key => $value) {
       $fields[$key] = self::truncateValue($value, 4096);
     }
 
     $this->fields = $fields;
     return $this;
   }
 
   public function getFields() {
     return $this->fields;
   }
 
   private static function truncateValue($value, $length) {
     if (is_string($value)) {
       if (strlen($value) <= $length) {
         return $value;
       } else {
         // NOTE: PhutilUTF8StringTruncator has huge runtime for giant strings.
         return phutil_utf8ize(substr($value, 0, $length)."\n<...>");
       }
     } else if (is_array($value)) {
       foreach ($value as $key => $v) {
         if ($length <= 0) {
           $value['<...>'] = '<...>';
           unset($value[$key]);
         } else {
           $v = self::truncateValue($v, $length);
           $length -= strlen($v);
           $value[$key] = $v;
         }
       }
       return $value;
     } else {
       return $value;
     }
   }
 
 }
diff --git a/src/applications/herald/storage/transcript/HeraldRuleTranscript.php b/src/applications/herald/storage/transcript/HeraldRuleTranscript.php
index 095907def..d12f1f9dd 100644
--- a/src/applications/herald/storage/transcript/HeraldRuleTranscript.php
+++ b/src/applications/herald/storage/transcript/HeraldRuleTranscript.php
@@ -1,56 +1,56 @@
 <?php
 
-final class HeraldRuleTranscript {
+final class HeraldRuleTranscript extends Phobject {
 
   protected $ruleID;
   protected $result;
   protected $reason;
 
   protected $ruleName;
   protected $ruleOwner;
 
   public function setResult($result) {
     $this->result = $result;
     return $this;
   }
 
   public function getResult() {
     return $this->result;
   }
 
   public function setReason($reason) {
     $this->reason = $reason;
     return $this;
   }
 
   public function getReason() {
     return $this->reason;
   }
 
   public function setRuleID($rule_id) {
     $this->ruleID = $rule_id;
     return $this;
   }
 
   public function getRuleID() {
     return $this->ruleID;
   }
 
   public function setRuleName($rule_name) {
     $this->ruleName = $rule_name;
     return $this;
   }
 
   public function getRuleName() {
     return $this->ruleName;
   }
 
   public function setRuleOwner($rule_owner) {
     $this->ruleOwner = $rule_owner;
     return $this;
   }
 
   public function getRuleOwner() {
     return $this->ruleOwner;
   }
 }
diff --git a/src/applications/legalpad/application/PhabricatorLegalpadApplication.php b/src/applications/legalpad/application/PhabricatorLegalpadApplication.php
index d7f7e0cee..e1394e050 100644
--- a/src/applications/legalpad/application/PhabricatorLegalpadApplication.php
+++ b/src/applications/legalpad/application/PhabricatorLegalpadApplication.php
@@ -1,96 +1,100 @@
 <?php
 
 final class PhabricatorLegalpadApplication extends PhabricatorApplication {
 
   public function getBaseURI() {
     return '/legalpad/';
   }
 
   public function getName() {
     return pht('Legalpad');
   }
 
   public function getShortDescription() {
     return pht('Agreements and Signatures');
   }
 
   public function getFontIcon() {
     return 'fa-gavel';
   }
 
   public function getTitleGlyph() {
     return "\xC2\xA9";
   }
 
   public function getApplicationGroup() {
     return self::GROUP_UTILITIES;
   }
 
   public function getRemarkupRules() {
     return array(
       new LegalpadDocumentRemarkupRule(),
     );
   }
 
   public function getHelpDocumentationArticles(PhabricatorUser $viewer) {
     return array(
       array(
         'name' => pht('Legalpad User Guide'),
         'href' => PhabricatorEnv::getDoclink('Legalpad User Guide'),
       ),
     );
   }
 
   public function getOverview() {
     return pht(
       '**Legalpad** is a simple application for tracking signatures and '.
       'legal agreements. At the moment, it is primarily intended to help '.
       'open source projects keep track of Contributor License Agreements.');
   }
 
   public function getRoutes() {
     return array(
       '/L(?P<id>\d+)' => 'LegalpadDocumentSignController',
       '/legalpad/' => array(
         '' => 'LegalpadDocumentListController',
         '(?:query/(?P<queryKey>[^/]+)/)?' => 'LegalpadDocumentListController',
         'create/' => 'LegalpadDocumentEditController',
         'edit/(?P<id>\d+)/' => 'LegalpadDocumentEditController',
         'comment/(?P<id>\d+)/' => 'LegalpadDocumentCommentController',
         'view/(?P<id>\d+)/' => 'LegalpadDocumentManageController',
         'done/' => 'LegalpadDocumentDoneController',
         'verify/(?P<code>[^/]+)/'
           => 'LegalpadDocumentSignatureVerificationController',
         'signatures/(?:(?P<id>\d+)/)?(?:query/(?P<queryKey>[^/]+)/)?'
           => 'LegalpadDocumentSignatureListController',
         'addsignature/(?P<id>\d+)/' => 'LegalpadDocumentSignatureAddController',
         'signature/(?P<id>\d+)/' => 'LegalpadDocumentSignatureViewController',
         'document/' => array(
           'preview/' => 'PhabricatorMarkupPreviewController',
         ),
       ),
     );
   }
 
   protected function getCustomCapabilities() {
     return array(
       LegalpadCreateDocumentsCapability::CAPABILITY => array(),
-      LegalpadDefaultViewCapability::CAPABILITY => array(),
-      LegalpadDefaultEditCapability::CAPABILITY => array(),
+      LegalpadDefaultViewCapability::CAPABILITY => array(
+        'template' => PhabricatorLegalpadDocumentPHIDType::TYPECONST,
+      ),
+      LegalpadDefaultEditCapability::CAPABILITY => array(
+        'template' => PhabricatorLegalpadDocumentPHIDType::TYPECONST,
+      ),
     );
   }
 
   public function getMailCommandObjects() {
     return array(
       'document' => array(
         'name' => pht('Email Commands: Legalpad Documents'),
         'header' => pht('Interacting with Legalpad Documents'),
         'object' => new LegalpadDocument(),
         'summary' => pht(
           'This page documents the commands you can use to interact with '.
           'documents in Legalpad.'),
       ),
     );
   }
 
 }
diff --git a/src/applications/lipsum/generator/PhabricatorTestDataGenerator.php b/src/applications/lipsum/generator/PhabricatorTestDataGenerator.php
index 9adf6fca7..9bff25a06 100644
--- a/src/applications/lipsum/generator/PhabricatorTestDataGenerator.php
+++ b/src/applications/lipsum/generator/PhabricatorTestDataGenerator.php
@@ -1,30 +1,30 @@
 <?php
 
-abstract class PhabricatorTestDataGenerator {
+abstract class PhabricatorTestDataGenerator extends Phobject {
 
   public function generate() {
     return;
   }
 
   public function loadOneRandom($classname) {
     try {
       return newv($classname, array())
         ->loadOneWhere('1 = 1 ORDER BY RAND() LIMIT 1');
     } catch (PhutilMissingSymbolException $ex) {
       throw new PhutilMissingSymbolException(
         pht(
           'Unable to load symbol %s: this class does not exit.',
           $classname));
     }
   }
 
 
   public function loadPhabrictorUserPHID() {
     return $this->loadOneRandom('PhabricatorUser')->getPHID();
   }
 
   public function loadPhabrictorUser() {
     return $this->loadOneRandom('PhabricatorUser');
   }
 
 }
diff --git a/src/applications/lipsum/image/PhabricatorLipsumArtist.php b/src/applications/lipsum/image/PhabricatorLipsumArtist.php
index 695f1a2f6..06bedcd69 100644
--- a/src/applications/lipsum/image/PhabricatorLipsumArtist.php
+++ b/src/applications/lipsum/image/PhabricatorLipsumArtist.php
@@ -1,68 +1,68 @@
 <?php
 
-abstract class PhabricatorLipsumArtist {
+abstract class PhabricatorLipsumArtist extends Phobject {
 
   protected function getHSBColor($h, $s, $b) {
     if ($s == 0) {
       $cr = $b;
       $cg = $b;
       $cb = $b;
     } else {
       $h /= 60;
       $i = (int)$h;
       $f = $h - $i;
       $p = $b * (1 - $s);
       $q = $b * (1 - $s * $f);
       $t = $b * (1 - $s * (1 - $f));
       switch ($i) {
         case 0:
           $cr = $b;
           $cg = $t;
           $cb = $p;
           break;
         case 1:
           $cr = $q;
           $cg = $b;
           $cb = $p;
           break;
         case 2:
           $cr = $p;
           $cg = $b;
           $cb = $t;
           break;
         case 3:
           $cr = $p;
           $cg = $q;
           $cb = $b;
           break;
         case 4:
           $cr = $t;
           $cg = $p;
           $cb = $b;
           break;
         default:
           $cr = $b;
           $cg = $p;
           $cb = $q;
           break;
       }
     }
 
     $cr = (int)round($cr * 255);
     $cg = (int)round($cg * 255);
     $cb = (int)round($cb * 255);
 
     return ($cr << 16) + ($cg << 8) + $cb;
   }
 
   public function generate($x, $y) {
     $image = imagecreatetruecolor($x, $y);
     $this->draw($image, $x, $y);
     return PhabricatorImageTransformer::saveImageDataInAnyFormat(
       $image,
       'image/jpeg');
   }
 
   abstract protected function draw($image, $x, $y);
 
 }
diff --git a/src/applications/macro/query/PhabricatorMacroSearchEngine.php b/src/applications/macro/query/PhabricatorMacroSearchEngine.php
index 5c69716bb..9caf1e2f3 100644
--- a/src/applications/macro/query/PhabricatorMacroSearchEngine.php
+++ b/src/applications/macro/query/PhabricatorMacroSearchEngine.php
@@ -1,184 +1,187 @@
 <?php
 
 final class PhabricatorMacroSearchEngine
   extends PhabricatorApplicationSearchEngine {
 
   public function getResultTypeDescription() {
     return pht('Macros');
   }
 
   public function getApplicationClassName() {
     return 'PhabricatorMacroApplication';
   }
 
   public function newQuery() {
     return id(new PhabricatorMacroQuery())
       ->needFiles(true);
   }
 
   protected function buildCustomSearchFields() {
     return array(
       id(new PhabricatorSearchSelectField())
         ->setLabel(pht('Status'))
         ->setKey('status')
         ->setOptions(PhabricatorMacroQuery::getStatusOptions()),
       id(new PhabricatorSearchUsersField())
         ->setLabel(pht('Authors'))
         ->setKey('authorPHIDs')
         ->setAliases(array('author', 'authors')),
       id(new PhabricatorSearchTextField())
         ->setLabel(pht('Name Contains'))
         ->setKey('nameLike'),
       id(new PhabricatorSearchStringListField())
         ->setLabel(pht('Exact Names'))
         ->setKey('names'),
       id(new PhabricatorSearchSelectField())
         ->setLabel(pht('Marked with Flag'))
         ->setKey('flagColor')
         ->setDefault('-1')
         ->setOptions(PhabricatorMacroQuery::getFlagColorsOptions()),
       id(new PhabricatorSearchDateField())
         ->setLabel(pht('Created After'))
         ->setKey('createdStart'),
       id(new PhabricatorSearchDateField())
         ->setLabel(pht('Created Before'))
         ->setKey('createdEnd'),
     );
   }
 
   protected function getDefaultFieldOrder() {
     return array(
       '...',
       'createdStart',
       'createdEnd',
     );
   }
 
   protected function buildQueryFromParameters(array $map) {
     $query = $this->newQuery();
 
     if ($map['authorPHIDs']) {
       $query->withAuthorPHIDs($map['authorPHIDs']);
     }
 
     if ($map['status']) {
       $query->withStatus($map['status']);
     }
 
     if ($map['names']) {
       $query->withNames($map['names']);
     }
 
     if (strlen($map['nameLike'])) {
       $query->withNameLike($map['nameLike']);
     }
 
     if ($map['createdStart']) {
       $query->withDateCreatedAfter($map['createdStart']);
     }
 
     if ($map['createdEnd']) {
       $query->withDateCreatedBefore($map['createdEnd']);
     }
 
     if ($map['flagColor'] !== null) {
       $query->withFlagColor($map['flagColor']);
     }
 
     return $query;
   }
 
   protected function getURI($path) {
     return '/macro/'.$path;
   }
 
   protected function getBuiltinQueryNames() {
     $names = array(
       'active'  => pht('Active'),
       'all'     => pht('All'),
     );
 
     if ($this->requireViewer()->isLoggedIn()) {
       $names['authored'] = pht('Authored');
     }
 
     return $names;
   }
 
   public function buildSavedQueryFromBuiltin($query_key) {
     $query = $this->newSavedQuery();
     $query->setQueryKey($query_key);
 
     switch ($query_key) {
       case 'active':
         return $query;
       case 'all':
         return $query->setParameter(
           'status',
           PhabricatorMacroQuery::STATUS_ANY);
       case 'authored':
         return $query->setParameter(
           'authorPHIDs',
           array($this->requireViewer()->getPHID()));
     }
 
     return parent::buildSavedQueryFromBuiltin($query_key);
   }
 
   protected function renderResultList(
     array $macros,
     PhabricatorSavedQuery $query,
     array $handles) {
 
     assert_instances_of($macros, 'PhabricatorFileImageMacro');
     $viewer = $this->requireViewer();
     $handles = $viewer->loadHandles(mpull($macros, 'getAuthorPHID'));
 
     $xform = PhabricatorFileTransform::getTransformByKey(
       PhabricatorFileThumbnailTransform::TRANSFORM_PINBOARD);
 
     $pinboard = new PHUIPinboardView();
     foreach ($macros as $macro) {
       $file = $macro->getFile();
 
-      $item = new PHUIPinboardItemView();
+      $item = id(new PHUIPinboardItemView())
+        ->setUser($viewer)
+        ->setObject($macro);
+
       if ($file) {
         $item->setImageURI($file->getURIForTransform($xform));
         list($x, $y) = $xform->getTransformedDimensions($file);
         $item->setImageSize($x, $y);
       }
 
       if ($macro->getDateCreated()) {
         $datetime = phabricator_date($macro->getDateCreated(), $viewer);
         $item->appendChild(
           phutil_tag(
             'div',
             array(),
             pht('Created on %s', $datetime)));
       } else {
         // Very old macros don't have a creation date. Rendering something
         // keeps all the pins at the same height and avoids flow issues.
         $item->appendChild(
           phutil_tag(
             'div',
             array(),
             pht('Created in ages long past')));
       }
 
       if ($macro->getAuthorPHID()) {
         $author_handle = $handles[$macro->getAuthorPHID()];
         $item->appendChild(
           pht('Created by %s', $author_handle->renderLink()));
       }
 
       $item->setURI($this->getApplicationURI('/view/'.$macro->getID().'/'));
       $item->setDisabled($macro->getisDisabled());
       $item->setHeader($macro->getName());
 
       $pinboard->addItem($item);
     }
 
     return $pinboard;
   }
 
 }
diff --git a/src/applications/maniphest/application/PhabricatorManiphestApplication.php b/src/applications/maniphest/application/PhabricatorManiphestApplication.php
index 3937d6be7..3debdb44c 100644
--- a/src/applications/maniphest/application/PhabricatorManiphestApplication.php
+++ b/src/applications/maniphest/application/PhabricatorManiphestApplication.php
@@ -1,167 +1,169 @@
 <?php
 
 final class PhabricatorManiphestApplication extends PhabricatorApplication {
 
   public function getName() {
     return pht('Maniphest');
   }
 
   public function getShortDescription() {
     return pht('Tasks and Bugs');
   }
 
   public function getBaseURI() {
     return '/maniphest/';
   }
 
   public function getFontIcon() {
     return 'fa-anchor';
   }
 
   public function getTitleGlyph() {
     return "\xE2\x9A\x93";
   }
 
   public function isPinnedByDefault(PhabricatorUser $viewer) {
     return true;
   }
 
   public function getApplicationOrder() {
     return 0.110;
   }
 
   public function getFactObjectsForAnalysis() {
     return array(
       new ManiphestTask(),
     );
   }
 
   public function getEventListeners() {
     return array(
       new ManiphestNameIndexEventListener(),
       new ManiphestHovercardEventListener(),
     );
   }
 
   public function getRemarkupRules() {
     return array(
       new ManiphestRemarkupRule(),
     );
   }
 
   public function getRoutes() {
     return array(
       '/T(?P<id>[1-9]\d*)' => 'ManiphestTaskDetailController',
       '/maniphest/' => array(
         '(?:query/(?P<queryKey>[^/]+)/)?' => 'ManiphestTaskListController',
         'report/(?:(?P<view>\w+)/)?' => 'ManiphestReportController',
         'batch/' => 'ManiphestBatchEditController',
         'task/' => array(
           'create/' => 'ManiphestTaskEditController',
           'edit/(?P<id>[1-9]\d*)/' => 'ManiphestTaskEditController',
           'descriptionpreview/'
             => 'PhabricatorMarkupPreviewController',
         ),
         'transaction/' => array(
           'save/' => 'ManiphestTransactionSaveController',
           'preview/(?P<id>[1-9]\d*)/'
             => 'ManiphestTransactionPreviewController',
         ),
         'export/(?P<key>[^/]+)/' => 'ManiphestExportController',
         'subpriority/' => 'ManiphestSubpriorityController',
       ),
     );
   }
 
   public function loadStatus(PhabricatorUser $user) {
     $status = array();
 
     if (!$user->isLoggedIn()) {
       return $status;
     }
 
     $query = id(new ManiphestTaskQuery())
       ->setViewer($user)
       ->withStatuses(ManiphestTaskStatus::getOpenStatusConstants())
       ->withOwners(array($user->getPHID()))
       ->setLimit(self::MAX_STATUS_ITEMS);
     $count = count($query->execute());
     $count_str = self::formatStatusCount(
       $count,
       '%s Assigned Tasks',
       '%d Assigned Task(s)');
 
     $type = PhabricatorApplicationStatusView::TYPE_WARNING;
     $status[] = id(new PhabricatorApplicationStatusView())
       ->setType($type)
       ->setText($count_str)
       ->setCount($count);
 
     return $status;
   }
 
   public function getQuickCreateItems(PhabricatorUser $viewer) {
     $items = array();
 
     $item = id(new PHUIListItemView())
       ->setName(pht('Maniphest Task'))
       ->setIcon('fa-anchor')
       ->setHref($this->getBaseURI().'task/create/');
     $items[] = $item;
 
     return $items;
   }
 
   public function supportsEmailIntegration() {
     return true;
   }
 
   public function getAppEmailBlurb() {
     return pht(
       'Send email to these addresses to create tasks. %s',
       phutil_tag(
         'a',
         array(
           'href' => $this->getInboundEmailSupportLink(),
         ),
         pht('Learn More')));
   }
 
   protected function getCustomCapabilities() {
     return array(
       ManiphestDefaultViewCapability::CAPABILITY => array(
         'caption' => pht('Default view policy for newly created tasks.'),
+        'template' => ManiphestTaskPHIDType::TYPECONST,
       ),
       ManiphestDefaultEditCapability::CAPABILITY => array(
         'caption' => pht('Default edit policy for newly created tasks.'),
+        'template' => ManiphestTaskPHIDType::TYPECONST,
       ),
       ManiphestEditStatusCapability::CAPABILITY => array(),
       ManiphestEditAssignCapability::CAPABILITY => array(),
       ManiphestEditPoliciesCapability::CAPABILITY => array(),
       ManiphestEditPriorityCapability::CAPABILITY => array(),
       ManiphestEditProjectsCapability::CAPABILITY => array(),
       ManiphestBulkEditCapability::CAPABILITY => array(),
     );
   }
 
   public function getMailCommandObjects() {
     return array(
       'task' => array(
         'name' => pht('Email Commands: Tasks'),
         'header' => pht('Interacting with Maniphest Tasks'),
         'object' => new ManiphestTask(),
         'summary' => pht(
           'This page documents the commands you can use to interact with '.
           'tasks in Maniphest. These commands work when creating new tasks '.
           'via email and when replying to existing tasks.'),
       ),
     );
   }
 
   public function getApplicationSearchDocumentTypes() {
     return array(
       ManiphestTaskPHIDType::TYPECONST,
     );
   }
 
 }
diff --git a/src/applications/maniphest/constants/ManiphestConstants.php b/src/applications/maniphest/constants/ManiphestConstants.php
index fa0d78b6a..7aba636a6 100644
--- a/src/applications/maniphest/constants/ManiphestConstants.php
+++ b/src/applications/maniphest/constants/ManiphestConstants.php
@@ -1,3 +1,3 @@
 <?php
 
-abstract class ManiphestConstants {}
+abstract class ManiphestConstants extends Phobject {}
diff --git a/src/applications/maniphest/controller/ManiphestBatchEditController.php b/src/applications/maniphest/controller/ManiphestBatchEditController.php
index 7817c55db..dadeb0260 100644
--- a/src/applications/maniphest/controller/ManiphestBatchEditController.php
+++ b/src/applications/maniphest/controller/ManiphestBatchEditController.php
@@ -1,406 +1,425 @@
 <?php
 
 final class ManiphestBatchEditController extends ManiphestController {
 
   public function handleRequest(AphrontRequest $request) {
     $viewer = $this->getViewer();
 
     $this->requireApplicationCapability(
       ManiphestBulkEditCapability::CAPABILITY);
 
     $project = null;
     $board_id = $request->getInt('board');
     if ($board_id) {
       $project = id(new PhabricatorProjectQuery())
         ->setViewer($viewer)
         ->withIDs(array($board_id))
         ->executeOne();
       if (!$project) {
         return new Aphront404Response();
       }
     }
 
     $task_ids = $request->getArr('batch');
     if (!$task_ids) {
       $task_ids = $request->getStrList('batch');
     }
 
     $tasks = id(new ManiphestTaskQuery())
       ->setViewer($viewer)
       ->withIDs($task_ids)
       ->requireCapabilities(
         array(
           PhabricatorPolicyCapability::CAN_VIEW,
           PhabricatorPolicyCapability::CAN_EDIT,
         ))
       ->needSubscriberPHIDs(true)
       ->needProjectPHIDs(true)
       ->execute();
 
     if ($project) {
       $cancel_uri = '/project/board/'.$project->getID().'/';
       $redirect_uri = $cancel_uri;
     } else {
       $cancel_uri = '/maniphest/';
       $redirect_uri = '/maniphest/?ids='.implode(',', mpull($tasks, 'getID'));
     }
 
     $actions = $request->getStr('actions');
     if ($actions) {
       $actions = phutil_json_decode($actions);
     }
 
     if ($request->isFormPost() && is_array($actions)) {
       foreach ($tasks as $task) {
         $field_list = PhabricatorCustomField::getObjectFields(
           $task,
           PhabricatorCustomField::ROLE_EDIT);
         $field_list->readFieldsFromStorage($task);
 
         $xactions = $this->buildTransactions($actions, $task);
         if ($xactions) {
           // TODO: Set content source to "batch edit".
 
           $editor = id(new ManiphestTransactionEditor())
             ->setActor($viewer)
             ->setContentSourceFromRequest($request)
             ->setContinueOnNoEffect(true)
             ->setContinueOnMissingFields(true)
             ->applyTransactions($task, $xactions);
         }
       }
 
       return id(new AphrontRedirectResponse())->setURI($redirect_uri);
     }
 
     $handles = ManiphestTaskListView::loadTaskHandles($viewer, $tasks);
 
     $list = new ManiphestTaskListView();
     $list->setTasks($tasks);
     $list->setUser($viewer);
     $list->setHandles($handles);
 
     $template = new AphrontTokenizerTemplateView();
     $template = $template->render();
 
     $projects_source = new PhabricatorProjectDatasource();
     $mailable_source = new PhabricatorMetaMTAMailableDatasource();
     $mailable_source->setViewer($viewer);
     $owner_source = new ManiphestAssigneeDatasource();
     $owner_source->setViewer($viewer);
+    $spaces_source = id(new PhabricatorSpacesNamespaceDatasource())
+      ->setViewer($viewer);
 
     require_celerity_resource('maniphest-batch-editor');
     Javelin::initBehavior(
       'maniphest-batch-editor',
       array(
         'root' => 'maniphest-batch-edit-form',
         'tokenizerTemplate' => $template,
         'sources' => array(
           'project' => array(
             'src' => $projects_source->getDatasourceURI(),
             'placeholder' => $projects_source->getPlaceholderText(),
             'browseURI' => $projects_source->getBrowseURI(),
           ),
           'owner' => array(
             'src' => $owner_source->getDatasourceURI(),
             'placeholder' => $owner_source->getPlaceholderText(),
             'browseURI' => $owner_source->getBrowseURI(),
             'limit' => 1,
           ),
           'cc' => array(
             'src' => $mailable_source->getDatasourceURI(),
             'placeholder' => $mailable_source->getPlaceholderText(),
             'browseURI' => $mailable_source->getBrowseURI(),
           ),
+          'spaces' => array(
+            'src' => $spaces_source->getDatasourceURI(),
+            'placeholder' => $spaces_source->getPlaceholderText(),
+            'browseURI' => $spaces_source->getBrowseURI(),
+            'limit' => 1,
+          ),
         ),
         'input' => 'batch-form-actions',
         'priorityMap' => ManiphestTaskPriority::getTaskPriorityMap(),
         'statusMap'   => ManiphestTaskStatus::getTaskStatusMap(),
       ));
 
     $form = id(new AphrontFormView())
       ->setUser($viewer)
       ->addHiddenInput('board', $board_id)
       ->setID('maniphest-batch-edit-form');
 
     foreach ($tasks as $task) {
       $form->appendChild(
         phutil_tag(
           'input',
           array(
             'type' => 'hidden',
             'name' => 'batch[]',
             'value' => $task->getID(),
           )));
     }
 
     $form->appendChild(
       phutil_tag(
         'input',
         array(
           'type' => 'hidden',
           'name' => 'actions',
           'id'   => 'batch-form-actions',
         )));
     $form->appendChild(
       id(new PHUIFormInsetView())
         ->setTitle(pht('Actions'))
         ->setRightButton(javelin_tag(
             'a',
             array(
               'href' => '#',
               'class' => 'button green',
               'sigil' => 'add-action',
               'mustcapture' => true,
             ),
             pht('Add Another Action')))
         ->setContent(javelin_tag(
           'table',
           array(
             'sigil' => 'maniphest-batch-actions',
             'class' => 'maniphest-batch-actions-table',
           ),
           '')))
       ->appendChild(
         id(new AphrontFormSubmitControl())
           ->setValue(pht('Update Tasks'))
           ->addCancelButton($cancel_uri));
 
     $title = pht('Batch Editor');
 
     $crumbs = $this->buildApplicationCrumbs();
     $crumbs->addTextCrumb($title);
 
     $task_box = id(new PHUIObjectBoxView())
       ->setHeaderText(pht('Selected Tasks'))
       ->setObjectList($list);
 
     $form_box = id(new PHUIObjectBoxView())
       ->setHeaderText(pht('Batch Editor'))
       ->setForm($form);
 
     return $this->buildApplicationPage(
       array(
         $crumbs,
         $task_box,
         $form_box,
       ),
       array(
         'title' => $title,
       ));
   }
 
   private function buildTransactions($actions, ManiphestTask $task) {
     $value_map = array();
     $type_map = array(
       'add_comment'     => PhabricatorTransactions::TYPE_COMMENT,
       'assign'          => ManiphestTransaction::TYPE_OWNER,
       'status'          => ManiphestTransaction::TYPE_STATUS,
       'priority'        => ManiphestTransaction::TYPE_PRIORITY,
       'add_project'     => PhabricatorTransactions::TYPE_EDGE,
       'remove_project'  => PhabricatorTransactions::TYPE_EDGE,
       'add_ccs'         => PhabricatorTransactions::TYPE_SUBSCRIBERS,
       'remove_ccs'      => PhabricatorTransactions::TYPE_SUBSCRIBERS,
+      'space' => PhabricatorTransactions::TYPE_SPACE,
     );
 
     $edge_edit_types = array(
       'add_project'    => true,
       'remove_project' => true,
       'add_ccs'        => true,
       'remove_ccs'     => true,
     );
 
     $xactions = array();
     foreach ($actions as $action) {
       if (empty($type_map[$action['action']])) {
         throw new Exception(pht("Unknown batch edit action '%s'!", $action));
       }
 
       $type = $type_map[$action['action']];
 
       // Figure out the current value, possibly after modifications by other
       // batch actions of the same type. For example, if the user chooses to
       // "Add Comment" twice, we should add both comments. More notably, if the
       // user chooses "Remove Project..." and also "Add Project...", we should
       // avoid restoring the removed project in the second transaction.
 
       if (array_key_exists($type, $value_map)) {
         $current = $value_map[$type];
       } else {
         switch ($type) {
           case PhabricatorTransactions::TYPE_COMMENT:
             $current = null;
             break;
           case ManiphestTransaction::TYPE_OWNER:
             $current = $task->getOwnerPHID();
             break;
           case ManiphestTransaction::TYPE_STATUS:
             $current = $task->getStatus();
             break;
           case ManiphestTransaction::TYPE_PRIORITY:
             $current = $task->getPriority();
             break;
           case PhabricatorTransactions::TYPE_EDGE:
             $current = $task->getProjectPHIDs();
             break;
           case PhabricatorTransactions::TYPE_SUBSCRIBERS:
             $current = $task->getSubscriberPHIDs();
             break;
+          case PhabricatorTransactions::TYPE_SPACE:
+            $current = PhabricatorSpacesNamespaceQuery::getObjectSpacePHID(
+              $task);
+            break;
         }
       }
 
       // Check if the value is meaningful / provided, and normalize it if
       // necessary. This discards, e.g., empty comments and empty owner
       // changes.
 
       $value = $action['value'];
       switch ($type) {
         case PhabricatorTransactions::TYPE_COMMENT:
           if (!strlen($value)) {
             continue 2;
           }
           break;
+        case PhabricatorTransactions::TYPE_SPACE:
+          if (empty($value)) {
+            continue 2;
+          }
+          $value = head($value);
+          break;
         case ManiphestTransaction::TYPE_OWNER:
           if (empty($value)) {
             continue 2;
           }
           $value = head($value);
           $no_owner = PhabricatorPeopleNoOwnerDatasource::FUNCTION_TOKEN;
           if ($value === $no_owner) {
             $value = null;
           }
           break;
         case PhabricatorTransactions::TYPE_EDGE:
           if (empty($value)) {
             continue 2;
           }
           break;
         case PhabricatorTransactions::TYPE_SUBSCRIBERS:
           if (empty($value)) {
             continue 2;
           }
           break;
       }
 
       // If the edit doesn't change anything, go to the next action. This
       // check is only valid for changes like "owner", "status", etc, not
       // for edge edits, because we should still apply an edit like
       // "Remove Projects: A, B" to a task with projects "A, B".
 
       if (empty($edge_edit_types[$action['action']])) {
         if ($value == $current) {
           continue;
         }
       }
 
       // Apply the value change; for most edits this is just replacement, but
       // some need to merge the current and edited values (add/remove project).
 
       switch ($type) {
         case PhabricatorTransactions::TYPE_COMMENT:
           if (strlen($current)) {
             $value = $current."\n\n".$value;
           }
           break;
         case PhabricatorTransactions::TYPE_EDGE:
           $is_remove = $action['action'] == 'remove_project';
 
           $current = array_fill_keys($current, true);
           $value   = array_fill_keys($value, true);
 
           $new = $current;
           $did_something = false;
 
           if ($is_remove) {
             foreach ($value as $phid => $ignored) {
               if (isset($new[$phid])) {
                 unset($new[$phid]);
                 $did_something = true;
               }
             }
           } else {
             foreach ($value as $phid => $ignored) {
               if (empty($new[$phid])) {
                 $new[$phid] = true;
                 $did_something = true;
               }
             }
           }
 
           if (!$did_something) {
             continue 2;
           }
 
           $value = array_keys($new);
           break;
         case PhabricatorTransactions::TYPE_SUBSCRIBERS:
           $is_remove = $action['action'] == 'remove_ccs';
 
           $current = array_fill_keys($current, true);
 
           $new = array();
           $did_something = false;
 
           if ($is_remove) {
             foreach ($value as $phid) {
               if (isset($current[$phid])) {
                 $new[$phid] = true;
                 $did_something = true;
               }
             }
             if ($new) {
               $value = array('-' => array_keys($new));
             }
           } else {
             $new = array();
             foreach ($value as $phid) {
               $new[$phid] = true;
               $did_something = true;
             }
             if ($new) {
               $value = array('+' => array_keys($new));
             }
           }
           if (!$did_something) {
             continue 2;
           }
 
           break;
       }
 
       $value_map[$type] = $value;
     }
 
     $template = new ManiphestTransaction();
 
     foreach ($value_map as $type => $value) {
       $xaction = clone $template;
       $xaction->setTransactionType($type);
 
       switch ($type) {
         case PhabricatorTransactions::TYPE_COMMENT:
           $xaction->attachComment(
             id(new ManiphestTransactionComment())
               ->setContent($value));
           break;
         case PhabricatorTransactions::TYPE_EDGE:
           $project_type = PhabricatorProjectObjectHasProjectEdgeType::EDGECONST;
           $xaction
             ->setMetadataValue('edge:type', $project_type)
             ->setNewValue(
               array(
                 '=' => array_fuse($value),
               ));
           break;
         default:
           $xaction->setNewValue($value);
           break;
       }
 
       $xactions[] = $xaction;
     }
 
     return $xactions;
   }
 
 }
diff --git a/src/applications/maniphest/controller/ManiphestTaskEditController.php b/src/applications/maniphest/controller/ManiphestTaskEditController.php
index d9a4e4438..72b75c876 100644
--- a/src/applications/maniphest/controller/ManiphestTaskEditController.php
+++ b/src/applications/maniphest/controller/ManiphestTaskEditController.php
@@ -1,754 +1,761 @@
 <?php
 
 final class ManiphestTaskEditController extends ManiphestController {
 
   private $id;
 
   public function willProcessRequest(array $data) {
     $this->id = idx($data, 'id');
   }
 
   public function processRequest() {
     $request = $this->getRequest();
     $user = $request->getUser();
 
     $response_type = $request->getStr('responseType', 'task');
     $order = $request->getStr('order', PhabricatorProjectColumn::DEFAULT_ORDER);
 
     $can_edit_assign = $this->hasApplicationCapability(
       ManiphestEditAssignCapability::CAPABILITY);
     $can_edit_policies = $this->hasApplicationCapability(
       ManiphestEditPoliciesCapability::CAPABILITY);
     $can_edit_priority = $this->hasApplicationCapability(
       ManiphestEditPriorityCapability::CAPABILITY);
     $can_edit_projects = $this->hasApplicationCapability(
       ManiphestEditProjectsCapability::CAPABILITY);
     $can_edit_status = $this->hasApplicationCapability(
       ManiphestEditStatusCapability::CAPABILITY);
     $can_create_projects = PhabricatorPolicyFilter::hasCapability(
       $user,
       PhabricatorApplication::getByClass('PhabricatorProjectApplication'),
       ProjectCreateProjectsCapability::CAPABILITY);
 
     $parent_task = null;
     $template_id = null;
 
     if ($this->id) {
       $task = id(new ManiphestTaskQuery())
         ->setViewer($user)
         ->requireCapabilities(
           array(
             PhabricatorPolicyCapability::CAN_VIEW,
             PhabricatorPolicyCapability::CAN_EDIT,
           ))
         ->withIDs(array($this->id))
         ->needSubscriberPHIDs(true)
         ->needProjectPHIDs(true)
         ->executeOne();
       if (!$task) {
         return new Aphront404Response();
       }
     } else {
       $task = ManiphestTask::initializeNewTask($user);
 
       // We currently do not allow you to set the task status when creating
       // a new task, although now that statuses are custom it might make
       // sense.
       $can_edit_status = false;
 
       // These allow task creation with defaults.
       if (!$request->isFormPost()) {
         $task->setTitle($request->getStr('title'));
 
         if ($can_edit_projects) {
           $projects = $request->getStr('projects');
           if ($projects) {
             $tokens = $request->getStrList('projects');
 
             $type_project = PhabricatorProjectProjectPHIDType::TYPECONST;
             foreach ($tokens as $key => $token) {
               if (phid_get_type($token) == $type_project) {
                 // If this is formatted like a PHID, leave it as-is.
                 continue;
               }
 
               if (preg_match('/^#/', $token)) {
                 // If this already has a "#", leave it as-is.
                 continue;
               }
 
               // Add a "#" prefix.
               $tokens[$key] = '#'.$token;
             }
 
             $default_projects = id(new PhabricatorObjectQuery())
               ->setViewer($user)
               ->withNames($tokens)
               ->execute();
             $default_projects = mpull($default_projects, 'getPHID');
 
             if ($default_projects) {
               $task->attachProjectPHIDs($default_projects);
             }
           }
         }
 
         if ($can_edit_priority) {
           $priority = $request->getInt('priority');
           if ($priority !== null) {
             $priority_map = ManiphestTaskPriority::getTaskPriorityMap();
             if (isset($priority_map[$priority])) {
                 $task->setPriority($priority);
             }
           }
         }
 
         $task->setDescription($request->getStr('description'));
 
         if ($can_edit_assign) {
           $assign = $request->getStr('assign');
           if (strlen($assign)) {
             $assign_user = id(new PhabricatorPeopleQuery())
               ->setViewer($user)
               ->withUsernames(array($assign))
               ->executeOne();
             if (!$assign_user) {
               $assign_user = id(new PhabricatorPeopleQuery())
                 ->setViewer($user)
                 ->withPHIDs(array($assign))
                 ->executeOne();
             }
 
             if ($assign_user) {
               $task->setOwnerPHID($assign_user->getPHID());
             }
           }
         }
       }
 
       $template_id = $request->getInt('template');
 
       // You can only have a parent task if you're creating a new task.
       $parent_id = $request->getInt('parent');
       if (strlen($parent_id)) {
         $parent_task = id(new ManiphestTaskQuery())
           ->setViewer($user)
           ->withIDs(array($parent_id))
           ->executeOne();
         if (!$parent_task) {
           return new Aphront404Response();
         }
         if (!$template_id) {
           $template_id = $parent_id;
         }
       }
     }
 
     $errors = array();
     $e_title = true;
 
     $field_list = PhabricatorCustomField::getObjectFields(
       $task,
       PhabricatorCustomField::ROLE_EDIT);
     $field_list->setViewer($user);
     $field_list->readFieldsFromStorage($task);
 
     $aux_fields = $field_list->getFields();
 
+    $v_space = $task->getSpacePHID();
+
     if ($request->isFormPost()) {
       $changes = array();
 
       $new_title = $request->getStr('title');
       $new_desc = $request->getStr('description');
       $new_status = $request->getStr('status');
+      $v_space = $request->getStr('spacePHID');
 
       if (!$task->getID()) {
         $workflow = 'create';
       } else {
         $workflow = '';
       }
 
       $changes[ManiphestTransaction::TYPE_TITLE] = $new_title;
       $changes[ManiphestTransaction::TYPE_DESCRIPTION] = $new_desc;
 
       if ($can_edit_status) {
         $changes[ManiphestTransaction::TYPE_STATUS] = $new_status;
       } else if (!$task->getID()) {
         // Create an initial status transaction for the burndown chart.
         // TODO: We can probably remove this once Facts comes online.
         $changes[ManiphestTransaction::TYPE_STATUS] = $task->getStatus();
       }
 
       $owner_tokenizer = $request->getArr('assigned_to');
       $owner_phid = reset($owner_tokenizer);
 
       if (!strlen($new_title)) {
         $e_title = pht('Required');
         $errors[] = pht('Title is required.');
       }
 
       $old_values = array();
       foreach ($aux_fields as $aux_arr_key => $aux_field) {
         // TODO: This should be buildFieldTransactionsFromRequest() once we
         // switch to ApplicationTransactions properly.
 
         $aux_old_value = $aux_field->getOldValueForApplicationTransactions();
         $aux_field->readValueFromRequest($request);
         $aux_new_value = $aux_field->getNewValueForApplicationTransactions();
 
         // TODO: We're faking a call to the ApplicaitonTransaction validation
         // logic here. We need valid objects to pass, but they aren't used
         // in a meaningful way. For now, build User objects. Once the Maniphest
         // objects exist, this will switch over automatically. This is a big
         // hack but shouldn't be long for this world.
         $placeholder_editor = new PhabricatorUserProfileEditor();
 
         $field_errors = $aux_field->validateApplicationTransactions(
           $placeholder_editor,
           PhabricatorTransactions::TYPE_CUSTOMFIELD,
           array(
             id(new ManiphestTransaction())
               ->setOldValue($aux_old_value)
               ->setNewValue($aux_new_value),
           ));
 
         foreach ($field_errors as $error) {
           $errors[] = $error->getMessage();
         }
 
         $old_values[$aux_field->getFieldKey()] = $aux_old_value;
       }
 
       if ($errors) {
         $task->setTitle($new_title);
         $task->setDescription($new_desc);
         $task->setPriority($request->getInt('priority'));
         $task->setOwnerPHID($owner_phid);
         $task->attachSubscriberPHIDs($request->getArr('cc'));
         $task->attachProjectPHIDs($request->getArr('projects'));
       } else {
 
         if ($can_edit_priority) {
           $changes[ManiphestTransaction::TYPE_PRIORITY] =
             $request->getInt('priority');
         }
         if ($can_edit_assign) {
           $changes[ManiphestTransaction::TYPE_OWNER] = $owner_phid;
         }
 
         $changes[PhabricatorTransactions::TYPE_SUBSCRIBERS] =
           array('=' => $request->getArr('cc'));
 
         if ($can_edit_projects) {
           $projects = $request->getArr('projects');
           $changes[PhabricatorTransactions::TYPE_EDGE] =
             $projects;
           $column_phid = $request->getStr('columnPHID');
           // allow for putting a task in a project column at creation -only-
           if (!$task->getID() && $column_phid && $projects) {
             $column = id(new PhabricatorProjectColumnQuery())
               ->setViewer($user)
               ->withProjectPHIDs($projects)
               ->withPHIDs(array($column_phid))
               ->executeOne();
             if ($column) {
               $changes[ManiphestTransaction::TYPE_PROJECT_COLUMN] =
                 array(
                   'new' => array(
                     'projectPHID' => $column->getProjectPHID(),
                     'columnPHIDs' => array($column_phid),
                   ),
                   'old' => array(
                     'projectPHID' => $column->getProjectPHID(),
                     'columnPHIDs' => array(),
                   ),
                 );
             }
           }
         }
 
         if ($can_edit_policies) {
+          $changes[PhabricatorTransactions::TYPE_SPACE] = $v_space;
           $changes[PhabricatorTransactions::TYPE_VIEW_POLICY] =
             $request->getStr('viewPolicy');
           $changes[PhabricatorTransactions::TYPE_EDIT_POLICY] =
             $request->getStr('editPolicy');
         }
 
         $template = new ManiphestTransaction();
         $transactions = array();
 
         foreach ($changes as $type => $value) {
           $transaction = clone $template;
           $transaction->setTransactionType($type);
           if ($type == ManiphestTransaction::TYPE_PROJECT_COLUMN) {
             $transaction->setNewValue($value['new']);
             $transaction->setOldValue($value['old']);
           } else if ($type == PhabricatorTransactions::TYPE_EDGE) {
             $project_type =
               PhabricatorProjectObjectHasProjectEdgeType::EDGECONST;
             $transaction
               ->setMetadataValue('edge:type', $project_type)
               ->setNewValue(
                 array(
                   '=' => array_fuse($value),
                 ));
           } else {
             $transaction->setNewValue($value);
           }
           $transactions[] = $transaction;
         }
 
         if ($aux_fields) {
           foreach ($aux_fields as $aux_field) {
             $transaction = clone $template;
             $transaction->setTransactionType(
               PhabricatorTransactions::TYPE_CUSTOMFIELD);
             $aux_key = $aux_field->getFieldKey();
             $transaction->setMetadataValue('customfield:key', $aux_key);
             $old = idx($old_values, $aux_key);
             $new = $aux_field->getNewValueForApplicationTransactions();
 
             $transaction->setOldValue($old);
             $transaction->setNewValue($new);
 
             $transactions[] = $transaction;
           }
         }
 
         if ($transactions) {
           $is_new = !$task->getID();
 
           $event = new PhabricatorEvent(
             PhabricatorEventType::TYPE_MANIPHEST_WILLEDITTASK,
             array(
               'task'          => $task,
               'new'           => $is_new,
               'transactions'  => $transactions,
             ));
           $event->setUser($user);
           $event->setAphrontRequest($request);
           PhutilEventEngine::dispatchEvent($event);
 
           $task = $event->getValue('task');
           $transactions = $event->getValue('transactions');
 
           $editor = id(new ManiphestTransactionEditor())
             ->setActor($user)
             ->setContentSourceFromRequest($request)
             ->setContinueOnNoEffect(true)
             ->applyTransactions($task, $transactions);
 
           $event = new PhabricatorEvent(
             PhabricatorEventType::TYPE_MANIPHEST_DIDEDITTASK,
             array(
               'task'          => $task,
               'new'           => $is_new,
               'transactions'  => $transactions,
             ));
           $event->setUser($user);
           $event->setAphrontRequest($request);
           PhutilEventEngine::dispatchEvent($event);
         }
 
 
         if ($parent_task) {
           // TODO: This should be transactional now.
           id(new PhabricatorEdgeEditor())
             ->addEdge(
               $parent_task->getPHID(),
               ManiphestTaskDependsOnTaskEdgeType::EDGECONST,
               $task->getPHID())
             ->save();
           $workflow = $parent_task->getID();
         }
 
         if ($request->isAjax()) {
           switch ($response_type) {
             case 'card':
               $owner = null;
               if ($task->getOwnerPHID()) {
                 $owner = id(new PhabricatorHandleQuery())
                   ->setViewer($user)
                   ->withPHIDs(array($task->getOwnerPHID()))
                   ->executeOne();
               }
               $tasks = id(new ProjectBoardTaskCard())
                 ->setViewer($user)
                 ->setTask($task)
                 ->setOwner($owner)
                 ->setCanEdit(true)
                 ->getItem();
 
               $column = id(new PhabricatorProjectColumnQuery())
                 ->setViewer($user)
                 ->withPHIDs(array($request->getStr('columnPHID')))
                 ->executeOne();
               if (!$column) {
                 return new Aphront404Response();
               }
 
               // re-load projects for accuracy as they are not re-loaded via
               // the editor
               $project_phids = PhabricatorEdgeQuery::loadDestinationPHIDs(
                 $task->getPHID(),
                 PhabricatorProjectObjectHasProjectEdgeType::EDGECONST);
               $task->attachProjectPHIDs($project_phids);
               $remove_from_board = false;
               if (!in_array($column->getProjectPHID(), $project_phids)) {
                 $remove_from_board = true;
               }
 
               $positions = id(new PhabricatorProjectColumnPositionQuery())
                 ->setViewer($user)
                 ->withColumns(array($column))
                 ->execute();
               $task_phids = mpull($positions, 'getObjectPHID');
 
               $column_tasks = id(new ManiphestTaskQuery())
                 ->setViewer($user)
                 ->withPHIDs($task_phids)
                 ->execute();
 
               if ($order == PhabricatorProjectColumn::ORDER_NATURAL) {
                 // TODO: This is a little bit awkward, because PHP and JS use
                 // slightly different sort order parameters to achieve the same
                 // effect. It would be good to unify this a bit at some point.
                 $sort_map = array();
                 foreach ($positions as $position) {
                   $sort_map[$position->getObjectPHID()] = array(
                     -$position->getSequence(),
                     $position->getID(),
                   );
                 }
               } else {
                 $sort_map = mpull(
                   $column_tasks,
                   'getPrioritySortVector',
                   'getPHID');
               }
 
               $data = array(
                 'sortMap' => $sort_map,
                 'removeFromBoard' => $remove_from_board,
               );
               break;
             case 'task':
             default:
               $tasks = $this->renderSingleTask($task);
               $data = array();
               break;
           }
           return id(new AphrontAjaxResponse())->setContent(
             array(
               'tasks' => $tasks,
               'data' => $data,
             ));
         }
 
         $redirect_uri = '/T'.$task->getID();
 
         if ($workflow) {
           $redirect_uri .= '?workflow='.$workflow;
         }
 
         return id(new AphrontRedirectResponse())
           ->setURI($redirect_uri);
       }
     } else {
       if (!$task->getID()) {
         $task->attachSubscriberPHIDs(array(
           $user->getPHID(),
         ));
         if ($template_id) {
           $template_task = id(new ManiphestTaskQuery())
             ->setViewer($user)
             ->withIDs(array($template_id))
             ->needSubscriberPHIDs(true)
             ->needProjectPHIDs(true)
             ->executeOne();
           if ($template_task) {
             $cc_phids = array_unique(array_merge(
               $template_task->getSubscriberPHIDs(),
               array($user->getPHID())));
             $task->attachSubscriberPHIDs($cc_phids);
             $task->attachProjectPHIDs($template_task->getProjectPHIDs());
             $task->setOwnerPHID($template_task->getOwnerPHID());
             $task->setPriority($template_task->getPriority());
             $task->setViewPolicy($template_task->getViewPolicy());
             $task->setEditPolicy($template_task->getEditPolicy());
 
+            $v_space = $template_task->getSpacePHID();
+
             $template_fields = PhabricatorCustomField::getObjectFields(
               $template_task,
               PhabricatorCustomField::ROLE_EDIT);
 
             $fields = $template_fields->getFields();
             foreach ($fields as $key => $field) {
               if (!$field->shouldCopyWhenCreatingSimilarTask()) {
                 unset($fields[$key]);
               }
               if (empty($aux_fields[$key])) {
                 unset($fields[$key]);
               }
             }
 
             if ($fields) {
               id(new PhabricatorCustomFieldList($fields))
                 ->setViewer($user)
                 ->readFieldsFromStorage($template_task);
 
               foreach ($fields as $key => $field) {
                 $aux_fields[$key]->setValueFromStorage(
                   $field->getValueForStorage());
               }
             }
           }
         }
       }
     }
 
     $error_view = null;
     if ($errors) {
       $error_view = new PHUIInfoView();
       $error_view->setErrors($errors);
     }
 
     $priority_map = ManiphestTaskPriority::getTaskPriorityMap();
 
     if ($task->getOwnerPHID()) {
       $assigned_value = array($task->getOwnerPHID());
     } else {
       $assigned_value = array();
     }
 
     if ($task->getSubscriberPHIDs()) {
       $cc_value = $task->getSubscriberPHIDs();
     } else {
       $cc_value = array();
     }
 
     if ($task->getProjectPHIDs()) {
       $projects_value = $task->getProjectPHIDs();
     } else {
       $projects_value = array();
     }
 
     $cancel_id = nonempty($task->getID(), $template_id);
     if ($cancel_id) {
       $cancel_uri = '/T'.$cancel_id;
     } else {
       $cancel_uri = '/maniphest/';
     }
 
     if ($task->getID()) {
       $button_name = pht('Save Task');
       $header_name = pht('Edit Task');
     } else if ($parent_task) {
       $cancel_uri = '/T'.$parent_task->getID();
       $button_name = pht('Create Task');
       $header_name = pht('Create New Subtask');
     } else {
       $button_name = pht('Create Task');
       $header_name = pht('Create New Task');
     }
 
     require_celerity_resource('maniphest-task-edit-css');
 
     $project_tokenizer_id = celerity_generate_unique_node_id();
 
     $form = new AphrontFormView();
     $form
       ->setUser($user)
       ->addHiddenInput('template', $template_id)
       ->addHiddenInput('responseType', $response_type)
       ->addHiddenInput('order', $order)
       ->addHiddenInput('ungrippable', $request->getStr('ungrippable'))
       ->addHiddenInput('columnPHID', $request->getStr('columnPHID'));
 
     if ($parent_task) {
       $form
         ->appendChild(
           id(new AphrontFormStaticControl())
             ->setLabel(pht('Parent Task'))
             ->setValue($user->renderHandle($parent_task->getPHID())))
         ->addHiddenInput('parent', $parent_task->getID());
     }
 
     $form
       ->appendChild(
         id(new AphrontFormTextAreaControl())
           ->setLabel(pht('Title'))
           ->setName('title')
           ->setError($e_title)
           ->setHeight(AphrontFormTextAreaControl::HEIGHT_VERY_SHORT)
           ->setValue($task->getTitle()));
 
     if ($can_edit_status) {
       // See T4819.
       $status_map = ManiphestTaskStatus::getTaskStatusMap();
       $dup_status = ManiphestTaskStatus::getDuplicateStatus();
 
       if ($task->getStatus() != $dup_status) {
         unset($status_map[$dup_status]);
       }
 
       $form
         ->appendChild(
           id(new AphrontFormSelectControl())
             ->setLabel(pht('Status'))
             ->setName('status')
             ->setValue($task->getStatus())
             ->setOptions($status_map));
     }
 
     $policies = id(new PhabricatorPolicyQuery())
       ->setViewer($user)
       ->setObject($task)
       ->execute();
 
     if ($can_edit_assign) {
       $form->appendControl(
         id(new AphrontFormTokenizerControl())
           ->setLabel(pht('Assigned To'))
           ->setName('assigned_to')
           ->setValue($assigned_value)
           ->setUser($user)
           ->setDatasource(new PhabricatorPeopleDatasource())
           ->setLimit(1));
     }
 
     $form
       ->appendControl(
         id(new AphrontFormTokenizerControl())
           ->setLabel(pht('CC'))
           ->setName('cc')
           ->setValue($cc_value)
           ->setUser($user)
           ->setDatasource(new PhabricatorMetaMTAMailableDatasource()));
 
     if ($can_edit_priority) {
       $form
         ->appendChild(
           id(new AphrontFormSelectControl())
             ->setLabel(pht('Priority'))
             ->setName('priority')
             ->setOptions($priority_map)
             ->setValue($task->getPriority()));
     }
 
     if ($can_edit_policies) {
       $form
         ->appendChild(
           id(new AphrontFormPolicyControl())
             ->setUser($user)
             ->setCapability(PhabricatorPolicyCapability::CAN_VIEW)
             ->setPolicyObject($task)
             ->setPolicies($policies)
+            ->setSpacePHID($v_space)
             ->setName('viewPolicy'))
         ->appendChild(
           id(new AphrontFormPolicyControl())
             ->setUser($user)
             ->setCapability(PhabricatorPolicyCapability::CAN_EDIT)
             ->setPolicyObject($task)
             ->setPolicies($policies)
             ->setName('editPolicy'));
     }
 
     if ($can_edit_projects) {
       $caption = null;
       if ($can_create_projects) {
         $caption = javelin_tag(
           'a',
           array(
             'href'        => '/project/create/',
             'mustcapture' => true,
             'sigil'       => 'project-create',
           ),
           pht('Create New Project'));
       }
       $form
         ->appendControl(
           id(new AphrontFormTokenizerControl())
             ->setLabel(pht('Projects'))
             ->setName('projects')
             ->setValue($projects_value)
             ->setID($project_tokenizer_id)
             ->setCaption($caption)
             ->setDatasource(new PhabricatorProjectDatasource()));
     }
 
     $field_list->appendFieldsToForm($form);
 
     require_celerity_resource('phui-info-view-css');
 
     Javelin::initBehavior('project-create', array(
       'tokenizerID' => $project_tokenizer_id,
     ));
 
     $description_control = id(new PhabricatorRemarkupControl())
       ->setLabel(pht('Description'))
       ->setName('description')
       ->setID('description-textarea')
       ->setValue($task->getDescription())
       ->setUser($user);
 
     $form
       ->appendChild($description_control);
 
     if ($request->isAjax()) {
       $dialog = id(new AphrontDialogView())
         ->setUser($user)
         ->setWidth(AphrontDialogView::WIDTH_FULL)
         ->setTitle($header_name)
         ->appendChild(
           array(
             $error_view,
             $form->buildLayoutView(),
           ))
         ->addCancelButton($cancel_uri)
         ->addSubmitButton($button_name);
       return id(new AphrontDialogResponse())->setDialog($dialog);
     }
 
     $form
       ->appendChild(
         id(new AphrontFormSubmitControl())
           ->addCancelButton($cancel_uri)
           ->setValue($button_name));
 
     $form_box = id(new PHUIObjectBoxView())
       ->setHeaderText($header_name)
       ->setFormErrors($errors)
       ->setForm($form);
 
     $preview = id(new PHUIRemarkupPreviewPanel())
       ->setHeader(pht('Description Preview'))
       ->setControlID('description-textarea')
       ->setPreviewURI($this->getApplicationURI('task/descriptionpreview/'));
 
     if ($task->getID()) {
       $page_objects = array($task->getPHID());
     } else {
       $page_objects = array();
     }
 
     $crumbs = $this->buildApplicationCrumbs();
 
     if ($task->getID()) {
       $crumbs->addTextCrumb('T'.$task->getID(), '/T'.$task->getID());
     }
 
     $crumbs->addTextCrumb($header_name);
 
     return $this->buildApplicationPage(
       array(
         $crumbs,
         $form_box,
         $preview,
       ),
       array(
         'title' => $header_name,
         'pageObjects' => $page_objects,
       ));
   }
 
 }
diff --git a/src/applications/maniphest/export/ManiphestExcelFormat.php b/src/applications/maniphest/export/ManiphestExcelFormat.php
index f22df6997..29c94f475 100644
--- a/src/applications/maniphest/export/ManiphestExcelFormat.php
+++ b/src/applications/maniphest/export/ManiphestExcelFormat.php
@@ -1,44 +1,44 @@
 <?php
 
-abstract class ManiphestExcelFormat {
+abstract class ManiphestExcelFormat extends Phobject {
 
   final public static function loadAllFormats() {
     $classes = id(new PhutilSymbolLoader())
       ->setAncestorClass(__CLASS__)
       ->setConcreteOnly(true)
       ->selectAndLoadSymbols();
 
     $objects = array();
     foreach ($classes as $class) {
       $objects[$class['name']] = newv($class['name'], array());
     }
 
     $objects = msort($objects, 'getOrder');
 
     return $objects;
   }
 
   abstract public function getName();
   abstract public function getFileName();
 
   public function getOrder() {
     return 0;
   }
 
   protected function computeExcelDate($epoch) {
     $seconds_per_day = (60 * 60 * 24);
     $offset = ($seconds_per_day * 25569);
 
     return ($epoch + $offset) / $seconds_per_day;
   }
 
   /**
    * @phutil-external-symbol class PHPExcel
    */
   abstract public function buildWorkbook(
     PHPExcel $workbook,
     array $tasks,
     array $handles,
     PhabricatorUser $user);
 
 }
diff --git a/src/applications/maniphest/export/__tests__/ManiphestExcelFormatTestCase.php b/src/applications/maniphest/export/__tests__/ManiphestExcelFormatTestCase.php
new file mode 100644
index 000000000..a3c312fcd
--- /dev/null
+++ b/src/applications/maniphest/export/__tests__/ManiphestExcelFormatTestCase.php
@@ -0,0 +1,10 @@
+<?php
+
+final class ManiphestExcelFormatTestCase extends PhabricatorTestCase {
+
+  public function testLoadAllFormats() {
+    ManiphestExcelFormat::loadAllFormats();
+    $this->assertTrue(true);
+  }
+
+}
diff --git a/src/applications/maniphest/policyrule/ManiphestTaskAuthorPolicyRule.php b/src/applications/maniphest/policyrule/ManiphestTaskAuthorPolicyRule.php
new file mode 100644
index 000000000..04a39ff71
--- /dev/null
+++ b/src/applications/maniphest/policyrule/ManiphestTaskAuthorPolicyRule.php
@@ -0,0 +1,43 @@
+<?php
+
+final class ManiphestTaskAuthorPolicyRule
+  extends PhabricatorPolicyRule {
+
+  public function getObjectPolicyKey() {
+    return 'maniphest.author';
+  }
+
+  public function getObjectPolicyName() {
+    return pht('Task Author');
+  }
+
+  public function getPolicyExplanation() {
+    return pht('The author of this task can take this action.');
+  }
+
+  public function getRuleDescription() {
+    return pht('task author');
+  }
+
+  public function canApplyToObject(PhabricatorPolicyInterface $object) {
+    return ($object instanceof ManiphestTask);
+  }
+
+  public function applyRule(
+    PhabricatorUser $viewer,
+    $value,
+    PhabricatorPolicyInterface $object) {
+
+    $viewer_phid = $viewer->getPHID();
+    if (!$viewer_phid) {
+      return false;
+    }
+
+    return ($object->getAuthorPHID() == $viewer_phid);
+  }
+
+  public function getValueControlType() {
+    return self::CONTROL_TYPE_NONE;
+  }
+
+}
diff --git a/src/applications/maniphest/query/ManiphestTaskQuery.php b/src/applications/maniphest/query/ManiphestTaskQuery.php
index 956810524..841a91731 100644
--- a/src/applications/maniphest/query/ManiphestTaskQuery.php
+++ b/src/applications/maniphest/query/ManiphestTaskQuery.php
@@ -1,849 +1,854 @@
 <?php
 
 /**
  * Query tasks by specific criteria. This class uses the higher-performance
  * but less-general Maniphest indexes to satisfy queries.
  */
 final class ManiphestTaskQuery extends PhabricatorCursorPagedPolicyAwareQuery {
 
   private $taskIDs             = array();
   private $taskPHIDs           = array();
   private $authorPHIDs         = array();
   private $ownerPHIDs          = array();
   private $noOwner;
   private $anyOwner;
   private $subscriberPHIDs     = array();
   private $dateCreatedAfter;
   private $dateCreatedBefore;
   private $dateModifiedAfter;
   private $dateModifiedBefore;
   private $subpriorityMin;
   private $subpriorityMax;
 
   private $fullTextSearch   = '';
 
   private $status           = 'status-any';
   const STATUS_ANY          = 'status-any';
   const STATUS_OPEN         = 'status-open';
   const STATUS_CLOSED       = 'status-closed';
   const STATUS_RESOLVED     = 'status-resolved';
   const STATUS_WONTFIX      = 'status-wontfix';
   const STATUS_INVALID      = 'status-invalid';
   const STATUS_SPITE        = 'status-spite';
   const STATUS_DUPLICATE    = 'status-duplicate';
 
   private $statuses;
   private $priorities;
   private $subpriorities;
 
   private $groupBy          = 'group-none';
   const GROUP_NONE          = 'group-none';
   const GROUP_PRIORITY      = 'group-priority';
   const GROUP_OWNER         = 'group-owner';
   const GROUP_STATUS        = 'group-status';
   const GROUP_PROJECT       = 'group-project';
 
   const ORDER_PRIORITY      = 'order-priority';
   const ORDER_CREATED       = 'order-created';
   const ORDER_MODIFIED      = 'order-modified';
   const ORDER_TITLE         = 'order-title';
 
   private $needSubscriberPHIDs;
   private $needProjectPHIDs;
   private $blockingTasks;
   private $blockedTasks;
 
   public function withAuthors(array $authors) {
     $this->authorPHIDs = $authors;
     return $this;
   }
 
   public function withIDs(array $ids) {
     $this->taskIDs = $ids;
     return $this;
   }
 
   public function withPHIDs(array $phids) {
     $this->taskPHIDs = $phids;
     return $this;
   }
 
   public function withOwners(array $owners) {
     $no_owner = PhabricatorPeopleNoOwnerDatasource::FUNCTION_TOKEN;
     $any_owner = PhabricatorPeopleAnyOwnerDatasource::FUNCTION_TOKEN;
 
     foreach ($owners as $k => $phid) {
       if ($phid === $no_owner || $phid === null) {
         $this->noOwner = true;
         unset($owners[$k]);
         break;
       }
       if ($phid === $any_owner) {
         $this->anyOwner = true;
         unset($owners[$k]);
         break;
       }
     }
     $this->ownerPHIDs = $owners;
     return $this;
   }
 
   public function withStatus($status) {
     $this->status = $status;
     return $this;
   }
 
   public function withStatuses(array $statuses) {
     $this->statuses = $statuses;
     return $this;
   }
 
   public function withPriorities(array $priorities) {
     $this->priorities = $priorities;
     return $this;
   }
 
   public function withSubpriorities(array $subpriorities) {
     $this->subpriorities = $subpriorities;
     return $this;
   }
 
   public function withSubpriorityBetween($min, $max) {
     $this->subpriorityMin = $min;
     $this->subpriorityMax = $max;
     return $this;
   }
 
   public function withSubscribers(array $subscribers) {
     $this->subscriberPHIDs = $subscribers;
     return $this;
   }
 
   public function withFullTextSearch($fulltext_search) {
     $this->fullTextSearch = $fulltext_search;
     return $this;
   }
 
   public function setGroupBy($group) {
     $this->groupBy = $group;
 
     switch ($this->groupBy) {
       case self::GROUP_NONE:
         $vector = array();
         break;
       case self::GROUP_PRIORITY:
         $vector = array('priority');
         break;
       case self::GROUP_OWNER:
         $vector = array('owner');
         break;
       case self::GROUP_STATUS:
         $vector = array('status');
         break;
       case self::GROUP_PROJECT:
         $vector = array('project');
         break;
     }
 
     $this->setGroupVector($vector);
 
     return $this;
   }
 
   /**
    * True returns tasks that are blocking other tasks only.
    * False returns tasks that are not blocking other tasks only.
    * Null returns tasks regardless of blocking status.
    */
   public function withBlockingTasks($mode) {
     $this->blockingTasks = $mode;
     return $this;
   }
 
   public function shouldJoinBlockingTasks() {
     return $this->blockingTasks !== null;
   }
 
   /**
    * True returns tasks that are blocked by other tasks only.
    * False returns tasks that are not blocked by other tasks only.
    * Null returns tasks regardless of blocked by status.
    */
   public function withBlockedTasks($mode) {
     $this->blockedTasks = $mode;
     return $this;
   }
 
   public function shouldJoinBlockedTasks() {
     return $this->blockedTasks !== null;
   }
 
   public function withDateCreatedBefore($date_created_before) {
     $this->dateCreatedBefore = $date_created_before;
     return $this;
   }
 
   public function withDateCreatedAfter($date_created_after) {
     $this->dateCreatedAfter = $date_created_after;
     return $this;
   }
 
   public function withDateModifiedBefore($date_modified_before) {
     $this->dateModifiedBefore = $date_modified_before;
     return $this;
   }
 
   public function withDateModifiedAfter($date_modified_after) {
     $this->dateModifiedAfter = $date_modified_after;
     return $this;
   }
 
   public function needSubscriberPHIDs($bool) {
     $this->needSubscriberPHIDs = $bool;
     return $this;
   }
 
   public function needProjectPHIDs($bool) {
     $this->needProjectPHIDs = $bool;
     return $this;
   }
 
   public function newResultObject() {
     return new ManiphestTask();
   }
 
   protected function loadPage() {
     $task_dao = new ManiphestTask();
     $conn = $task_dao->establishConnection('r');
 
     $where = array();
     $where[] = $this->buildTaskIDsWhereClause($conn);
     $where[] = $this->buildTaskPHIDsWhereClause($conn);
     $where[] = $this->buildStatusWhereClause($conn);
     $where[] = $this->buildStatusesWhereClause($conn);
     $where[] = $this->buildDependenciesWhereClause($conn);
     $where[] = $this->buildAuthorWhereClause($conn);
     $where[] = $this->buildOwnerWhereClause($conn);
     $where[] = $this->buildFullTextWhereClause($conn);
 
     if ($this->dateCreatedAfter) {
       $where[] = qsprintf(
         $conn,
         'task.dateCreated >= %d',
         $this->dateCreatedAfter);
     }
 
     if ($this->dateCreatedBefore) {
       $where[] = qsprintf(
         $conn,
         'task.dateCreated <= %d',
         $this->dateCreatedBefore);
     }
 
     if ($this->dateModifiedAfter) {
       $where[] = qsprintf(
         $conn,
         'task.dateModified >= %d',
         $this->dateModifiedAfter);
     }
 
     if ($this->dateModifiedBefore) {
       $where[] = qsprintf(
         $conn,
         'task.dateModified <= %d',
         $this->dateModifiedBefore);
     }
 
     if ($this->priorities) {
       $where[] = qsprintf(
         $conn,
         'task.priority IN (%Ld)',
         $this->priorities);
     }
 
     if ($this->subpriorities) {
       $where[] = qsprintf(
         $conn,
         'task.subpriority IN (%Lf)',
         $this->subpriorities);
     }
 
     if ($this->subpriorityMin) {
       $where[] = qsprintf(
         $conn,
         'task.subpriority >= %f',
         $this->subpriorityMin);
     }
 
     if ($this->subpriorityMax) {
       $where[] = qsprintf(
         $conn,
         'task.subpriority <= %f',
         $this->subpriorityMax);
     }
 
     $where[] = $this->buildWhereClauseParts($conn);
 
     $where = $this->formatWhereClause($where);
 
     $group_column = '';
     switch ($this->groupBy) {
       case self::GROUP_PROJECT:
         $group_column = qsprintf(
           $conn,
           ', projectGroupName.indexedObjectPHID projectGroupPHID');
         break;
     }
 
     $rows = queryfx_all(
       $conn,
       '%Q %Q FROM %T task %Q %Q %Q %Q %Q %Q',
       $this->buildSelectClause($conn),
       $group_column,
       $task_dao->getTableName(),
       $this->buildJoinClause($conn),
       $where,
       $this->buildGroupClause($conn),
       $this->buildHavingClause($conn),
       $this->buildOrderClause($conn),
       $this->buildLimitClause($conn));
 
     switch ($this->groupBy) {
       case self::GROUP_PROJECT:
         $data = ipull($rows, null, 'id');
         break;
       default:
         $data = $rows;
         break;
     }
 
     $tasks = $task_dao->loadAllFromArray($data);
 
     switch ($this->groupBy) {
       case self::GROUP_PROJECT:
         $results = array();
         foreach ($rows as $row) {
           $task = clone $tasks[$row['id']];
           $task->attachGroupByProjectPHID($row['projectGroupPHID']);
           $results[] = $task;
         }
         $tasks = $results;
         break;
     }
 
     return $tasks;
   }
 
   protected function willFilterPage(array $tasks) {
     if ($this->groupBy == self::GROUP_PROJECT) {
       // We should only return project groups which the user can actually see.
       $project_phids = mpull($tasks, 'getGroupByProjectPHID');
       $projects = id(new PhabricatorProjectQuery())
         ->setViewer($this->getViewer())
         ->withPHIDs($project_phids)
         ->execute();
       $projects = mpull($projects, null, 'getPHID');
 
       foreach ($tasks as $key => $task) {
         if (!$task->getGroupByProjectPHID()) {
           // This task is either not in any projects, or only in projects
           // which we're ignoring because they're being queried for explicitly.
           continue;
         }
 
         if (empty($projects[$task->getGroupByProjectPHID()])) {
           unset($tasks[$key]);
         }
       }
     }
 
     return $tasks;
   }
 
   protected function didFilterPage(array $tasks) {
     $phids = mpull($tasks, 'getPHID');
 
     if ($this->needProjectPHIDs) {
       $edge_query = id(new PhabricatorEdgeQuery())
         ->withSourcePHIDs($phids)
         ->withEdgeTypes(
           array(
             PhabricatorProjectObjectHasProjectEdgeType::EDGECONST,
           ));
       $edge_query->execute();
 
       foreach ($tasks as $task) {
         $project_phids = $edge_query->getDestinationPHIDs(
           array($task->getPHID()));
         $task->attachProjectPHIDs($project_phids);
       }
     }
 
     if ($this->needSubscriberPHIDs) {
       $subscriber_sets = id(new PhabricatorSubscribersQuery())
         ->withObjectPHIDs($phids)
         ->execute();
       foreach ($tasks as $task) {
         $subscribers = idx($subscriber_sets, $task->getPHID(), array());
         $task->attachSubscriberPHIDs($subscribers);
       }
     }
 
     return $tasks;
   }
 
   private function buildTaskIDsWhereClause(AphrontDatabaseConnection $conn) {
     if (!$this->taskIDs) {
       return null;
     }
 
     return qsprintf(
       $conn,
       'task.id in (%Ld)',
       $this->taskIDs);
   }
 
   private function buildTaskPHIDsWhereClause(AphrontDatabaseConnection $conn) {
     if (!$this->taskPHIDs) {
       return null;
     }
 
     return qsprintf(
       $conn,
       'task.phid in (%Ls)',
       $this->taskPHIDs);
   }
 
   private function buildStatusWhereClause(AphrontDatabaseConnection $conn) {
     static $map = array(
       self::STATUS_RESOLVED   => ManiphestTaskStatus::STATUS_CLOSED_RESOLVED,
       self::STATUS_WONTFIX    => ManiphestTaskStatus::STATUS_CLOSED_WONTFIX,
       self::STATUS_INVALID    => ManiphestTaskStatus::STATUS_CLOSED_INVALID,
       self::STATUS_SPITE      => ManiphestTaskStatus::STATUS_CLOSED_SPITE,
       self::STATUS_DUPLICATE  => ManiphestTaskStatus::STATUS_CLOSED_DUPLICATE,
     );
 
     switch ($this->status) {
       case self::STATUS_ANY:
         return null;
       case self::STATUS_OPEN:
         return qsprintf(
           $conn,
           'task.status IN (%Ls)',
           ManiphestTaskStatus::getOpenStatusConstants());
       case self::STATUS_CLOSED:
         return qsprintf(
           $conn,
           'task.status IN (%Ls)',
           ManiphestTaskStatus::getClosedStatusConstants());
       default:
         $constant = idx($map, $this->status);
         if (!$constant) {
           throw new Exception(pht("Unknown status query '%s'!", $this->status));
         }
         return qsprintf(
           $conn,
           'task.status = %s',
           $constant);
     }
   }
 
   private function buildStatusesWhereClause(AphrontDatabaseConnection $conn) {
     if ($this->statuses) {
       return qsprintf(
         $conn,
         'task.status IN (%Ls)',
         $this->statuses);
     }
     return null;
   }
 
   private function buildAuthorWhereClause(AphrontDatabaseConnection $conn) {
     if (!$this->authorPHIDs) {
       return null;
     }
 
     return qsprintf(
       $conn,
       'task.authorPHID in (%Ls)',
       $this->authorPHIDs);
   }
 
   private function buildOwnerWhereClause(AphrontDatabaseConnection $conn) {
     $subclause = array();
 
     if ($this->noOwner) {
       $subclause[] = qsprintf(
         $conn,
         'task.ownerPHID IS NULL');
     }
 
     if ($this->anyOwner) {
       $subclause[] = qsprintf(
         $conn,
         'task.ownerPHID IS NOT NULL');
     }
 
     if ($this->ownerPHIDs) {
       $subclause[] = qsprintf(
         $conn,
         'task.ownerPHID IN (%Ls)',
         $this->ownerPHIDs);
     }
 
     if (!$subclause) {
       return '';
     }
 
     return '('.implode(') OR (', $subclause).')';
   }
 
   private function buildFullTextWhereClause(AphrontDatabaseConnection $conn) {
     if (!strlen($this->fullTextSearch)) {
       return null;
     }
 
     // In doing a fulltext search, we first find all the PHIDs that match the
     // fulltext search, and then use that to limit the rest of the search
     $fulltext_query = id(new PhabricatorSavedQuery())
       ->setEngineClassName('PhabricatorSearchApplicationSearchEngine')
       ->setParameter('query', $this->fullTextSearch);
 
     // NOTE: Setting this to something larger than 2^53 will raise errors in
     // ElasticSearch, and billions of results won't fit in memory anyway.
     $fulltext_query->setParameter('limit', 100000);
     $fulltext_query->setParameter('types',
       array(ManiphestTaskPHIDType::TYPECONST));
 
     $engine = PhabricatorSearchEngine::loadEngine();
     $fulltext_results = $engine->executeSearch($fulltext_query);
 
     if (empty($fulltext_results)) {
       $fulltext_results = array(null);
     }
 
     return qsprintf(
       $conn,
       'task.phid IN (%Ls)',
       $fulltext_results);
   }
 
   private function buildDependenciesWhereClause(
     AphrontDatabaseConnection $conn) {
 
     if (!$this->shouldJoinBlockedTasks() &&
         !$this->shouldJoinBlockingTasks()) {
       return null;
     }
 
     $parts = array();
     if ($this->blockingTasks === true) {
       $parts[] = qsprintf(
         $conn,
         'blocking.dst IS NOT NULL AND blockingtask.status IN (%Ls)',
         ManiphestTaskStatus::getOpenStatusConstants());
     } else if ($this->blockingTasks === false) {
       $parts[] = qsprintf(
         $conn,
         'blocking.dst IS NULL OR blockingtask.status NOT IN (%Ls)',
         ManiphestTaskStatus::getOpenStatusConstants());
     }
 
     if ($this->blockedTasks === true) {
       $parts[] = qsprintf(
         $conn,
         'blocked.dst IS NOT NULL AND blockedtask.status IN (%Ls)',
         ManiphestTaskStatus::getOpenStatusConstants());
     } else if ($this->blockedTasks === false) {
       $parts[] = qsprintf(
         $conn,
         'blocked.dst IS NULL OR blockedtask.status NOT IN (%Ls)',
         ManiphestTaskStatus::getOpenStatusConstants());
     }
 
     return '('.implode(') OR (', $parts).')';
   }
 
   protected function buildJoinClauseParts(AphrontDatabaseConnection $conn_r) {
     $edge_table = PhabricatorEdgeConfig::TABLE_NAME_EDGE;
 
     $joins = array();
 
     if ($this->shouldJoinBlockingTasks()) {
       $joins[] = qsprintf(
         $conn_r,
         'LEFT JOIN %T blocking ON blocking.src = task.phid '.
         'AND blocking.type = %d '.
         'LEFT JOIN %T blockingtask ON blocking.dst = blockingtask.phid',
         $edge_table,
         ManiphestTaskDependedOnByTaskEdgeType::EDGECONST,
         id(new ManiphestTask())->getTableName());
     }
     if ($this->shouldJoinBlockedTasks()) {
       $joins[] = qsprintf(
         $conn_r,
         'LEFT JOIN %T blocked ON blocked.src = task.phid '.
         'AND blocked.type = %d '.
         'LEFT JOIN %T blockedtask ON blocked.dst = blockedtask.phid',
         $edge_table,
         ManiphestTaskDependsOnTaskEdgeType::EDGECONST,
         id(new ManiphestTask())->getTableName());
     }
 
     if ($this->subscriberPHIDs) {
       $joins[] = qsprintf(
         $conn_r,
         'JOIN %T e_ccs ON e_ccs.src = task.phid '.
         'AND e_ccs.type = %s '.
         'AND e_ccs.dst in (%Ls)',
         PhabricatorEdgeConfig::TABLE_NAME_EDGE,
         PhabricatorObjectHasSubscriberEdgeType::EDGECONST,
         $this->subscriberPHIDs);
     }
 
     switch ($this->groupBy) {
       case self::GROUP_PROJECT:
         $ignore_group_phids = $this->getIgnoreGroupedProjectPHIDs();
         if ($ignore_group_phids) {
           $joins[] = qsprintf(
             $conn_r,
             'LEFT JOIN %T projectGroup ON task.phid = projectGroup.src
               AND projectGroup.type = %d
               AND projectGroup.dst NOT IN (%Ls)',
             $edge_table,
             PhabricatorProjectObjectHasProjectEdgeType::EDGECONST,
             $ignore_group_phids);
         } else {
           $joins[] = qsprintf(
             $conn_r,
             'LEFT JOIN %T projectGroup ON task.phid = projectGroup.src
               AND projectGroup.type = %d',
             $edge_table,
             PhabricatorProjectObjectHasProjectEdgeType::EDGECONST);
         }
         $joins[] = qsprintf(
           $conn_r,
           'LEFT JOIN %T projectGroupName
             ON projectGroup.dst = projectGroupName.indexedObjectPHID',
           id(new ManiphestNameIndex())->getTableName());
         break;
     }
 
     $joins[] = parent::buildJoinClauseParts($conn_r);
 
     return $joins;
   }
 
   protected function buildGroupClause(AphrontDatabaseConnection $conn_r) {
     $joined_multiple_rows = $this->shouldJoinBlockingTasks() ||
                             $this->shouldJoinBlockedTasks() ||
                             ($this->shouldGroupQueryResultRows());
 
     $joined_project_name = ($this->groupBy == self::GROUP_PROJECT);
 
     // If we're joining multiple rows, we need to group the results by the
     // task IDs.
     if ($joined_multiple_rows) {
       if ($joined_project_name) {
         return 'GROUP BY task.phid, projectGroup.dst';
       } else {
         return 'GROUP BY task.phid';
       }
     } else {
       return '';
     }
   }
 
   /**
    * Return project PHIDs which we should ignore when grouping tasks by
    * project. For example, if a user issues a query like:
    *
    *   Tasks in all projects: Frontend, Bugs
    *
    * ...then we don't show "Frontend" or "Bugs" groups in the result set, since
    * they're meaningless as all results are in both groups.
    *
    * Similarly, for queries like:
    *
    *   Tasks in any projects: Public Relations
    *
    * ...we ignore the single project, as every result is in that project. (In
    * the case that there are several "any" projects, we do not ignore them.)
    *
    * @return list<phid> Project PHIDs which should be ignored in query
    *                    construction.
    */
   private function getIgnoreGroupedProjectPHIDs() {
     // Maybe we should also exclude the "OPERATOR_NOT" PHIDs? It won't
     // impact the results, but we might end up with a better query plan.
     // Investigate this on real data? This is likely very rare.
 
     $edge_types = array(
       PhabricatorProjectObjectHasProjectEdgeType::EDGECONST,
     );
 
     $phids = array();
 
     $phids[] = $this->getEdgeLogicValues(
       $edge_types,
       array(
         PhabricatorQueryConstraint::OPERATOR_AND,
       ));
 
     $any = $this->getEdgeLogicValues(
       $edge_types,
       array(
         PhabricatorQueryConstraint::OPERATOR_OR,
       ));
     if (count($any) == 1) {
       $phids[] = $any;
     }
 
     return array_mergev($phids);
   }
 
   protected function getResultCursor($result) {
     $id = $result->getID();
 
     if ($this->groupBy == self::GROUP_PROJECT) {
       return rtrim($id.'.'.$result->getGroupByProjectPHID(), '.');
     }
 
     return $id;
   }
 
   public function getBuiltinOrders() {
     $orders = array(
       'priority' => array(
         'vector' => array('priority', 'subpriority', 'id'),
         'name' => pht('Priority'),
         'aliases' => array(self::ORDER_PRIORITY),
       ),
       'updated' => array(
         'vector' => array('updated', 'id'),
-        'name' => pht('Date Updated'),
+        'name' => pht('Date Updated (Latest First)'),
         'aliases' => array(self::ORDER_MODIFIED),
       ),
+      'outdated' => array(
+        'vector' => array('-updated', '-id'),
+        'name' => pht('Date Updated (Oldest First)'),
+       ),
       'title' => array(
         'vector' => array('title', 'id'),
         'name' => pht('Title'),
         'aliases' => array(self::ORDER_TITLE),
       ),
     ) + parent::getBuiltinOrders();
 
     // Alias the "newest" builtin to the historical key for it.
     $orders['newest']['aliases'][] = self::ORDER_CREATED;
 
     $orders = array_select_keys(
       $orders,
       array(
         'priority',
         'updated',
+        'outdated',
         'newest',
         'oldest',
         'title',
       )) + $orders;
 
     return $orders;
   }
 
   public function getOrderableColumns() {
     return parent::getOrderableColumns() + array(
       'priority' => array(
         'table' => 'task',
         'column' => 'priority',
         'type' => 'int',
       ),
       'owner' => array(
         'table' => 'task',
         'column' => 'ownerOrdering',
         'null' => 'head',
         'reverse' => true,
         'type' => 'string',
       ),
       'status' => array(
         'table' => 'task',
         'column' => 'status',
         'type' => 'string',
         'reverse' => true,
       ),
       'project' => array(
         'table' => 'projectGroupName',
         'column' => 'indexedObjectName',
         'type' => 'string',
         'null' => 'head',
         'reverse' => true,
       ),
       'title' => array(
         'table' => 'task',
         'column' => 'title',
         'type' => 'string',
         'reverse' => true,
       ),
       'subpriority' => array(
         'table' => 'task',
         'column' => 'subpriority',
         'type' => 'float',
       ),
       'updated' => array(
         'table' => 'task',
         'column' => 'dateModified',
         'type' => 'int',
       ),
     );
   }
 
   protected function getPagingValueMap($cursor, array $keys) {
     $cursor_parts = explode('.', $cursor, 2);
     $task_id = $cursor_parts[0];
     $group_id = idx($cursor_parts, 1);
 
     $task = $this->loadCursorObject($task_id);
 
     $map = array(
       'id' => $task->getID(),
       'priority' => $task->getPriority(),
       'subpriority' => $task->getSubpriority(),
       'owner' => $task->getOwnerOrdering(),
       'status' => $task->getStatus(),
       'title' => $task->getTitle(),
       'updated' => $task->getDateModified(),
     );
 
     foreach ($keys as $key) {
       switch ($key) {
         case 'project':
           $value = null;
           if ($group_id) {
             $paging_projects = id(new PhabricatorProjectQuery())
               ->setViewer($this->getViewer())
               ->withPHIDs(array($group_id))
               ->execute();
             if ($paging_projects) {
               $value = head($paging_projects)->getName();
             }
           }
           $map[$key] = $value;
           break;
       }
     }
 
     foreach ($keys as $key) {
       if ($this->isCustomFieldOrderKey($key)) {
         $map += $this->getPagingValueMapForCustomFields($task);
         break;
       }
     }
 
     return $map;
   }
 
   protected function getPrimaryTableAlias() {
     return 'task';
   }
 
   public function getQueryApplicationClass() {
     return 'PhabricatorManiphestApplication';
   }
 
 }
diff --git a/src/applications/maniphest/query/ManiphestTaskSearchEngine.php b/src/applications/maniphest/query/ManiphestTaskSearchEngine.php
index 8a875202f..ae5b5353c 100644
--- a/src/applications/maniphest/query/ManiphestTaskSearchEngine.php
+++ b/src/applications/maniphest/query/ManiphestTaskSearchEngine.php
@@ -1,376 +1,376 @@
 <?php
 
 final class ManiphestTaskSearchEngine
   extends PhabricatorApplicationSearchEngine {
 
   private $showBatchControls;
   private $baseURI;
   private $isBoardView;
 
   public function setIsBoardView($is_board_view) {
     $this->isBoardView = $is_board_view;
     return $this;
   }
 
   public function getIsBoardView() {
     return $this->isBoardView;
   }
 
   public function setBaseURI($base_uri) {
     $this->baseURI = $base_uri;
     return $this;
   }
 
   public function getBaseURI() {
     return $this->baseURI;
   }
 
   public function setShowBatchControls($show_batch_controls) {
     $this->showBatchControls = $show_batch_controls;
     return $this;
   }
 
   public function getResultTypeDescription() {
     return pht('Tasks');
   }
 
   public function getApplicationClassName() {
     return 'PhabricatorManiphestApplication';
   }
 
   public function newQuery() {
     return id(new ManiphestTaskQuery())
       ->needProjectPHIDs(true);
   }
 
-  public function buildCustomSearchFields() {
+  protected function buildCustomSearchFields() {
     return array(
       id(new PhabricatorSearchOwnersField())
         ->setLabel(pht('Assigned To'))
         ->setKey('assignedPHIDs')
         ->setAliases(array('assigned')),
       id(new PhabricatorSearchUsersField())
         ->setLabel(pht('Authors'))
         ->setKey('authorPHIDs')
         ->setAliases(array('author', 'authors')),
       id(new PhabricatorSearchDatasourceField())
         ->setLabel(pht('Statuses'))
         ->setKey('statuses')
         ->setAliases(array('status'))
         ->setDatasource(new ManiphestTaskStatusFunctionDatasource()),
       id(new PhabricatorSearchDatasourceField())
         ->setLabel(pht('Priorities'))
         ->setKey('priorities')
         ->setAliases(array('priority'))
         ->setDatasource(new ManiphestTaskPriorityDatasource()),
       id(new PhabricatorSearchTextField())
         ->setLabel(pht('Contains Words'))
         ->setKey('fulltext'),
       id(new PhabricatorSearchThreeStateField())
         ->setLabel(pht('Blocking'))
         ->setKey('blocking')
         ->setOptions(
           pht('(Show All)'),
           pht('Show Only Tasks Blocking Other Tasks'),
           pht('Hide Tasks Blocking Other Tasks')),
       id(new PhabricatorSearchThreeStateField())
         ->setLabel(pht('Blocked'))
         ->setKey('blocked')
         ->setOptions(
           pht('(Show All)'),
           pht('Show Only Task Blocked By Other Tasks'),
           pht('Hide Tasks Blocked By Other Tasks')),
       id(new PhabricatorSearchSelectField())
         ->setLabel(pht('Group By'))
         ->setKey('group')
         ->setOptions($this->getGroupOptions()),
       id(new PhabricatorSearchStringListField())
         ->setLabel(pht('Task IDs'))
         ->setKey('ids'),
       id(new PhabricatorSearchDateField())
         ->setLabel(pht('Created After'))
         ->setKey('createdStart'),
       id(new PhabricatorSearchDateField())
         ->setLabel(pht('Created Before'))
         ->setKey('createdEnd'),
       id(new PhabricatorSearchDateField())
         ->setLabel(pht('Updated After'))
         ->setKey('modifiedStart'),
       id(new PhabricatorSearchDateField())
         ->setLabel(pht('Updated Before'))
         ->setKey('modifiedEnd'),
       id(new PhabricatorSearchTextField())
         ->setLabel(pht('Page Size'))
         ->setKey('limit'),
     );
   }
 
-  public function getDefaultFieldOrder() {
+  protected function getDefaultFieldOrder() {
     return array(
       'assignedPHIDs',
       'projectPHIDs',
       'authorPHIDs',
       'subscriberPHIDs',
       'statuses',
       'priorities',
       'fulltext',
       'blocking',
       'blocked',
       'group',
       'order',
       'ids',
       '...',
       'createdStart',
       'createdEnd',
       'modifiedStart',
       'modifiedEnd',
       'limit',
     );
   }
 
-  public function getHiddenFields() {
+  protected function getHiddenFields() {
     $keys = array();
 
     if ($this->getIsBoardView()) {
       $keys[] = 'group';
       $keys[] = 'order';
       $keys[] = 'limit';
     }
 
     return $keys;
   }
 
-  public function buildQueryFromParameters(array $map) {
+  protected function buildQueryFromParameters(array $map) {
     $query = id(new ManiphestTaskQuery())
       ->needProjectPHIDs(true);
 
     if ($map['assignedPHIDs']) {
       $query->withOwners($map['assignedPHIDs']);
     }
 
     if ($map['authorPHIDs']) {
       $query->withAuthors($map['authorPHIDs']);
     }
 
     if ($map['statuses']) {
       $query->withStatuses($map['statuses']);
     }
 
     if ($map['priorities']) {
       $query->withPriorities($map['priorities']);
     }
 
     if ($map['createdStart']) {
       $query->withDateCreatedAfter($map['createdStart']);
     }
 
     if ($map['createdEnd']) {
       $query->withDateCreatedBefore($map['createdEnd']);
     }
 
     if ($map['modifiedStart']) {
       $query->withDateModifiedAfter($map['modifiedStart']);
     }
 
     if ($map['modifiedEnd']) {
       $query->withDateModifiedBefore($map['modifiedEnd']);
     }
 
     if ($map['blocking'] !== null) {
       $query->withBlockingTasks($map['blocking']);
     }
 
     if ($map['blocked'] !== null) {
       $query->withBlockedTasks($map['blocked']);
     }
 
     if (strlen($map['fulltext'])) {
       $query->withFullTextSearch($map['fulltext']);
     }
 
     $group = idx($map, 'group');
     $group = idx($this->getGroupValues(), $group);
     if ($group) {
       $query->setGroupBy($group);
     } else {
       $query->setGroupBy(head($this->getGroupValues()));
     }
 
     if ($map['ids']) {
       $ids = $map['ids'];
       foreach ($ids as $key => $id) {
         $id = trim($id, ' Tt');
         if (!$id || !is_numeric($id)) {
           unset($ids[$key]);
         } else {
           $ids[$key] = $id;
         }
       }
 
       if ($ids) {
         $query->withIDs($ids);
       }
     }
 
     return $query;
   }
 
   protected function getURI($path) {
     if ($this->baseURI) {
       return $this->baseURI.$path;
     }
     return '/maniphest/'.$path;
   }
 
   protected function getBuiltinQueryNames() {
     $names = array();
 
     if ($this->requireViewer()->isLoggedIn()) {
       $names['assigned'] = pht('Assigned');
       $names['authored'] = pht('Authored');
       $names['subscribed'] = pht('Subscribed');
     }
 
     $names['open'] = pht('Open Tasks');
     $names['all'] = pht('All Tasks');
 
     return $names;
   }
 
   public function buildSavedQueryFromBuiltin($query_key) {
 
     $query = $this->newSavedQuery();
     $query->setQueryKey($query_key);
 
     $viewer_phid = $this->requireViewer()->getPHID();
 
     switch ($query_key) {
       case 'all':
         return $query;
       case 'assigned':
         return $query
           ->setParameter('assignedPHIDs', array($viewer_phid))
           ->setParameter(
             'statuses',
             ManiphestTaskStatus::getOpenStatusConstants());
       case 'subscribed':
         return $query
           ->setParameter('subscriberPHIDs', array($viewer_phid))
           ->setParameter(
             'statuses',
             ManiphestTaskStatus::getOpenStatusConstants());
       case 'open':
         return $query
           ->setParameter(
             'statuses',
             ManiphestTaskStatus::getOpenStatusConstants());
       case 'authored':
         return $query
           ->setParameter('authorPHIDs', array($viewer_phid))
           ->setParameter('order', 'created')
           ->setParameter('group', 'none');
     }
 
     return parent::buildSavedQueryFromBuiltin($query_key);
   }
 
   private function getGroupOptions() {
     return array(
       'priority' => pht('Priority'),
       'assigned' => pht('Assigned'),
       'status'   => pht('Status'),
       'project'  => pht('Project'),
       'none'     => pht('None'),
     );
   }
 
   private function getGroupValues() {
     return array(
       'priority' => ManiphestTaskQuery::GROUP_PRIORITY,
       'assigned' => ManiphestTaskQuery::GROUP_OWNER,
       'status'   => ManiphestTaskQuery::GROUP_STATUS,
       'project'  => ManiphestTaskQuery::GROUP_PROJECT,
       'none'     => ManiphestTaskQuery::GROUP_NONE,
     );
   }
 
   protected function renderResultList(
     array $tasks,
     PhabricatorSavedQuery $saved,
     array $handles) {
 
     $viewer = $this->requireViewer();
 
     if ($this->isPanelContext()) {
       $can_edit_priority = false;
       $can_bulk_edit = false;
     } else {
       $can_edit_priority = PhabricatorPolicyFilter::hasCapability(
         $viewer,
         $this->getApplication(),
         ManiphestEditPriorityCapability::CAPABILITY);
 
       $can_bulk_edit = PhabricatorPolicyFilter::hasCapability(
         $viewer,
         $this->getApplication(),
         ManiphestBulkEditCapability::CAPABILITY);
     }
 
     return id(new ManiphestTaskResultListView())
       ->setUser($viewer)
       ->setTasks($tasks)
       ->setSavedQuery($saved)
       ->setCanEditPriority($can_edit_priority)
       ->setCanBatchEdit($can_bulk_edit)
       ->setShowBatchControls($this->showBatchControls);
   }
 
   protected function willUseSavedQuery(PhabricatorSavedQuery $saved) {
 
     // The 'withUnassigned' parameter may be present in old saved queries from
     // before parameterized typeaheads, and is retained for compatibility. We
     // could remove it by migrating old saved queries.
     $assigned_phids = $saved->getParameter('assignedPHIDs', array());
     if ($saved->getParameter('withUnassigned')) {
       $assigned_phids[] = PhabricatorPeopleNoOwnerDatasource::FUNCTION_TOKEN;
     }
     $saved->setParameter('assignedPHIDs', $assigned_phids);
 
     // The 'projects' and other parameters may be present in old saved queries
     // from before parameterized typeaheads.
     $project_phids = $saved->getParameter('projectPHIDs', array());
 
     $old = $saved->getParameter('projects', array());
     foreach ($old as $phid) {
       $project_phids[] = $phid;
     }
 
     $all = $saved->getParameter('allProjectPHIDs', array());
     foreach ($all as $phid) {
       $project_phids[] = $phid;
     }
 
     $any = $saved->getParameter('anyProjectPHIDs', array());
     foreach ($any as $phid) {
       $project_phids[] = 'any('.$phid.')';
     }
 
     $not = $saved->getParameter('excludeProjectPHIDs', array());
     foreach ($not as $phid) {
       $project_phids[] = 'not('.$phid.')';
     }
 
     $users = $saved->getParameter('userProjectPHIDs', array());
     foreach ($users as $phid) {
       $project_phids[] = 'projects('.$phid.')';
     }
 
     $no = $saved->getParameter('withNoProject');
     if ($no) {
       $project_phids[] = 'null()';
     }
 
     $saved->setParameter('projectPHIDs', $project_phids);
   }
 
 }
diff --git a/src/applications/maniphest/storage/ManiphestTask.php b/src/applications/maniphest/storage/ManiphestTask.php
index cdf4b4a57..7fbc806ba 100644
--- a/src/applications/maniphest/storage/ManiphestTask.php
+++ b/src/applications/maniphest/storage/ManiphestTask.php
@@ -1,382 +1,393 @@
 <?php
 
 final class ManiphestTask extends ManiphestDAO
   implements
     PhabricatorSubscribableInterface,
     PhabricatorMarkupInterface,
     PhabricatorPolicyInterface,
     PhabricatorTokenReceiverInterface,
     PhabricatorFlaggableInterface,
     PhabricatorMentionableInterface,
     PhrequentTrackableInterface,
     PhabricatorCustomFieldInterface,
     PhabricatorDestructibleInterface,
     PhabricatorApplicationTransactionInterface,
-    PhabricatorProjectInterface {
+    PhabricatorProjectInterface,
+    PhabricatorSpacesInterface {
 
   const MARKUP_FIELD_DESCRIPTION = 'markup:desc';
 
   protected $authorPHID;
   protected $ownerPHID;
 
   protected $status;
   protected $priority;
   protected $subpriority = 0;
 
   protected $title = '';
   protected $originalTitle = '';
   protected $description = '';
   protected $originalEmailSource;
   protected $mailKey;
   protected $viewPolicy = PhabricatorPolicies::POLICY_USER;
   protected $editPolicy = PhabricatorPolicies::POLICY_USER;
 
   protected $attached = array();
   protected $projectPHIDs = array();
 
   protected $ownerOrdering;
+  protected $spacePHID;
 
   private $subscriberPHIDs = self::ATTACHABLE;
   private $groupByProjectPHID = self::ATTACHABLE;
   private $customFields = self::ATTACHABLE;
   private $edgeProjectPHIDs = self::ATTACHABLE;
 
   public static function initializeNewTask(PhabricatorUser $actor) {
     $app = id(new PhabricatorApplicationQuery())
       ->setViewer($actor)
       ->withClasses(array('PhabricatorManiphestApplication'))
       ->executeOne();
 
     $view_policy = $app->getPolicy(ManiphestDefaultViewCapability::CAPABILITY);
     $edit_policy = $app->getPolicy(ManiphestDefaultEditCapability::CAPABILITY);
 
     return id(new ManiphestTask())
       ->setStatus(ManiphestTaskStatus::getDefaultStatus())
       ->setPriority(ManiphestTaskPriority::getDefaultPriority())
       ->setAuthorPHID($actor->getPHID())
       ->setViewPolicy($view_policy)
       ->setEditPolicy($edit_policy)
+      ->setSpacePHID($actor->getDefaultSpacePHID())
       ->attachProjectPHIDs(array())
       ->attachSubscriberPHIDs(array());
   }
 
   protected function getConfiguration() {
     return array(
       self::CONFIG_AUX_PHID => true,
       self::CONFIG_SERIALIZATION => array(
         'ccPHIDs' => self::SERIALIZATION_JSON,
         'attached' => self::SERIALIZATION_JSON,
         'projectPHIDs' => self::SERIALIZATION_JSON,
       ),
       self::CONFIG_COLUMN_SCHEMA => array(
         'ownerPHID' => 'phid?',
         'status' => 'text12',
         'priority' => 'uint32',
         'title' => 'sort',
         'originalTitle' => 'text',
         'description' => 'text',
         'mailKey' => 'bytes20',
         'ownerOrdering' => 'text64?',
         'originalEmailSource' => 'text255?',
         'subpriority' => 'double',
 
         // T6203/NULLABILITY
         // This should not be nullable. It's going away soon anyway.
         'ccPHIDs' => 'text?',
 
       ),
       self::CONFIG_KEY_SCHEMA => array(
         'key_phid' => null,
         'phid' => array(
           'columns' => array('phid'),
           'unique' => true,
         ),
         'priority' => array(
           'columns' => array('priority', 'status'),
         ),
         'status' => array(
           'columns' => array('status'),
         ),
         'ownerPHID' => array(
           'columns' => array('ownerPHID', 'status'),
         ),
         'authorPHID' => array(
           'columns' => array('authorPHID', 'status'),
         ),
         'ownerOrdering' => array(
           'columns' => array('ownerOrdering'),
         ),
         'priority_2' => array(
           'columns' => array('priority', 'subpriority'),
         ),
         'key_dateCreated' => array(
           'columns' => array('dateCreated'),
         ),
         'key_dateModified' => array(
           'columns' => array('dateModified'),
         ),
         'key_title' => array(
           'columns' => array('title(64)'),
         ),
       ),
     ) + parent::getConfiguration();
   }
 
   public function loadDependsOnTaskPHIDs() {
     return PhabricatorEdgeQuery::loadDestinationPHIDs(
       $this->getPHID(),
       ManiphestTaskDependsOnTaskEdgeType::EDGECONST);
   }
 
   public function loadDependedOnByTaskPHIDs() {
     return PhabricatorEdgeQuery::loadDestinationPHIDs(
       $this->getPHID(),
       ManiphestTaskDependedOnByTaskEdgeType::EDGECONST);
   }
 
   public function getAttachedPHIDs($type) {
     return array_keys(idx($this->attached, $type, array()));
   }
 
   public function generatePHID() {
     return PhabricatorPHID::generateNewPHID(ManiphestTaskPHIDType::TYPECONST);
   }
 
   public function getSubscriberPHIDs() {
     return $this->assertAttached($this->subscriberPHIDs);
   }
 
   public function getProjectPHIDs() {
     return $this->assertAttached($this->edgeProjectPHIDs);
   }
 
   public function attachProjectPHIDs(array $phids) {
     $this->edgeProjectPHIDs = $phids;
     return $this;
   }
 
   public function attachSubscriberPHIDs(array $phids) {
     $this->subscriberPHIDs = $phids;
     return $this;
   }
 
   public function setOwnerPHID($phid) {
     $this->ownerPHID = nonempty($phid, null);
     return $this;
   }
 
   public function setTitle($title) {
     $this->title = $title;
     if (!$this->getID()) {
       $this->originalTitle = $title;
     }
     return $this;
   }
 
   public function getMonogram() {
     return 'T'.$this->getID();
   }
 
   public function attachGroupByProjectPHID($phid) {
     $this->groupByProjectPHID = $phid;
     return $this;
   }
 
   public function getGroupByProjectPHID() {
     return $this->assertAttached($this->groupByProjectPHID);
   }
 
   public function save() {
     if (!$this->mailKey) {
       $this->mailKey = Filesystem::readRandomCharacters(20);
     }
 
     $result = parent::save();
 
     return $result;
   }
 
   public function isClosed() {
     return ManiphestTaskStatus::isClosedStatus($this->getStatus());
   }
 
   public function getPrioritySortVector() {
     return array(
       $this->getPriority(),
       -$this->getSubpriority(),
       $this->getID(),
     );
   }
 
 
 /* -(  PhabricatorSubscribableInterface  )----------------------------------- */
 
 
   public function isAutomaticallySubscribed($phid) {
     return ($phid == $this->getOwnerPHID());
   }
 
   public function shouldShowSubscribersProperty() {
     return true;
   }
 
   public function shouldAllowSubscription($phid) {
     return true;
   }
 
 
 /* -(  Markup Interface  )--------------------------------------------------- */
 
 
   /**
    * @task markup
    */
   public function getMarkupFieldKey($field) {
     $hash = PhabricatorHash::digest($this->getMarkupText($field));
     $id = $this->getID();
     return "maniphest:T{$id}:{$field}:{$hash}";
   }
 
 
   /**
    * @task markup
    */
   public function getMarkupText($field) {
     return $this->getDescription();
   }
 
 
   /**
    * @task markup
    */
   public function newMarkupEngine($field) {
     return PhabricatorMarkupEngine::newManiphestMarkupEngine();
   }
 
 
   /**
    * @task markup
    */
   public function didMarkupText(
     $field,
     $output,
     PhutilMarkupEngine $engine) {
     return $output;
   }
 
 
   /**
    * @task markup
    */
   public function shouldUseMarkupCache($field) {
     return (bool)$this->getID();
   }
 
 
 /* -(  Policy Interface  )--------------------------------------------------- */
 
 
   public function getCapabilities() {
     return array(
       PhabricatorPolicyCapability::CAN_VIEW,
       PhabricatorPolicyCapability::CAN_EDIT,
     );
   }
 
   public function getPolicy($capability) {
     switch ($capability) {
       case PhabricatorPolicyCapability::CAN_VIEW:
         return $this->getViewPolicy();
       case PhabricatorPolicyCapability::CAN_EDIT:
         return $this->getEditPolicy();
     }
   }
 
   public function hasAutomaticCapability($capability, PhabricatorUser $user) {
     // The owner of a task can always view and edit it.
     $owner_phid = $this->getOwnerPHID();
     if ($owner_phid) {
       $user_phid = $user->getPHID();
       if ($user_phid == $owner_phid) {
         return true;
       }
     }
 
     return false;
   }
 
   public function describeAutomaticCapability($capability) {
     return pht('The owner of a task can always view and edit it.');
   }
 
 
 /* -(  PhabricatorTokenReceiverInterface  )---------------------------------- */
 
 
   public function getUsersToNotifyOfTokenGiven() {
     // Sort of ambiguous who this was intended for; just let them both know.
     return array_filter(
       array_unique(
         array(
           $this->getAuthorPHID(),
           $this->getOwnerPHID(),
         )));
   }
 
 
 /* -(  PhabricatorCustomFieldInterface  )------------------------------------ */
 
 
   public function getCustomFieldSpecificationForRole($role) {
     return PhabricatorEnv::getEnvConfig('maniphest.fields');
   }
 
   public function getCustomFieldBaseClass() {
     return 'ManiphestCustomField';
   }
 
   public function getCustomFields() {
     return $this->assertAttached($this->customFields);
   }
 
   public function attachCustomFields(PhabricatorCustomFieldAttachment $fields) {
     $this->customFields = $fields;
     return $this;
   }
 
 
 /* -(  PhabricatorDestructibleInterface  )----------------------------------- */
 
 
   public function destroyObjectPermanently(
     PhabricatorDestructionEngine $engine) {
 
     $this->openTransaction();
     $this->delete();
     $this->saveTransaction();
   }
 
 
 /* -(  PhabricatorApplicationTransactionInterface  )------------------------- */
 
 
   public function getApplicationTransactionEditor() {
     return new ManiphestTransactionEditor();
   }
 
   public function getApplicationTransactionObject() {
     return $this;
   }
 
   public function getApplicationTransactionTemplate() {
     return new ManiphestTransaction();
   }
 
   public function willRenderTimeline(
     PhabricatorApplicationTransactionView $timeline,
     AphrontRequest $request) {
 
     return $timeline;
   }
 
+
+/* -(  PhabricatorSpacesInterface  )----------------------------------------- */
+
+
+  public function getSpacePHID() {
+    return $this->spacePHID;
+  }
+
 }
diff --git a/src/applications/maniphest/view/ManiphestTaskListView.php b/src/applications/maniphest/view/ManiphestTaskListView.php
index efab4c42f..799bb0ae3 100644
--- a/src/applications/maniphest/view/ManiphestTaskListView.php
+++ b/src/applications/maniphest/view/ManiphestTaskListView.php
@@ -1,150 +1,152 @@
 <?php
 
 final class ManiphestTaskListView extends ManiphestView {
 
   private $tasks;
   private $handles;
   private $showBatchControls;
   private $showSubpriorityControls;
   private $noDataString;
 
   public function setTasks(array $tasks) {
     assert_instances_of($tasks, 'ManiphestTask');
     $this->tasks = $tasks;
     return $this;
   }
 
   public function setHandles(array $handles) {
     assert_instances_of($handles, 'PhabricatorObjectHandle');
     $this->handles = $handles;
     return $this;
   }
 
   public function setShowBatchControls($show_batch_controls) {
     $this->showBatchControls = $show_batch_controls;
     return $this;
   }
 
   public function setShowSubpriorityControls($show_subpriority_controls) {
     $this->showSubpriorityControls = $show_subpriority_controls;
     return $this;
   }
 
   public function setNoDataString($text) {
     $this->noDataString = $text;
     return $this;
   }
 
   public function render() {
     $handles = $this->handles;
 
     require_celerity_resource('maniphest-task-summary-css');
 
     $list = new PHUIObjectItemListView();
 
     if ($this->noDataString) {
       $list->setNoDataString($this->noDataString);
     } else {
       $list->setNoDataString(pht('No tasks.'));
     }
 
     $status_map = ManiphestTaskStatus::getTaskStatusMap();
     $color_map = ManiphestTaskPriority::getColorMap();
 
     if ($this->showBatchControls) {
       Javelin::initBehavior('maniphest-list-editor');
     }
 
     foreach ($this->tasks as $task) {
-      $item = new PHUIObjectItemView();
-      $item->setObjectName('T'.$task->getID());
-      $item->setHeader($task->getTitle());
-      $item->setHref('/T'.$task->getID());
+      $item = id(new PHUIObjectItemView())
+        ->setUser($this->getUser())
+        ->setObject($task)
+        ->setObjectName('T'.$task->getID())
+        ->setHeader($task->getTitle())
+        ->setHref('/T'.$task->getID());
 
       if ($task->getOwnerPHID()) {
         $owner = $handles[$task->getOwnerPHID()];
         $item->addByline(pht('Assigned: %s', $owner->renderLink()));
       }
 
       $status = $task->getStatus();
       // TODO: redesign-2015 move icon map to maniphest.statuses
       $icon = 'fa-exclamation-circle ';
       $icon .= idx($color_map, $task->getPriority(), 'grey');
       if ($task->isClosed()) {
         $item->setDisabled(true);
         $icon = 'fa-check-square-o grey';
       }
       $item->setStatusIcon($icon, idx($status_map, $task->getStatus()));
 
       $item->addIcon(
         'none',
         phabricator_datetime($task->getDateModified(), $this->getUser()));
 
       if ($this->showSubpriorityControls) {
         $item->setGrippable(true);
       }
       if ($this->showSubpriorityControls || $this->showBatchControls) {
         $item->addSigil('maniphest-task');
       }
 
       $project_handles = array_select_keys(
         $handles,
         $task->getProjectPHIDs());
 
       $item->addAttribute(
         id(new PHUIHandleTagListView())
           ->setLimit(4)
           ->setNoDataString(pht('No Projects'))
           ->setSlim(true)
           ->setHandles($project_handles));
 
       $item->setMetadata(
         array(
           'taskID' => $task->getID(),
         ));
 
       if ($this->showBatchControls) {
         $href = new PhutilURI('/maniphest/task/edit/'.$task->getID().'/');
         if (!$this->showSubpriorityControls) {
           $href->setQueryParam('ungrippable', 'true');
         }
         $item->addAction(
           id(new PHUIListItemView())
             ->setIcon('fa-pencil')
             ->addSigil('maniphest-edit-task')
             ->setHref($href));
       }
 
       $list->addItem($item);
     }
 
     return $list;
   }
 
   public static function loadTaskHandles(
     PhabricatorUser $viewer,
     array $tasks) {
     assert_instances_of($tasks, 'ManiphestTask');
 
     $phids = array();
     foreach ($tasks as $task) {
       $assigned_phid = $task->getOwnerPHID();
       if ($assigned_phid) {
         $phids[] = $assigned_phid;
       }
       foreach ($task->getProjectPHIDs() as $project_phid) {
         $phids[] = $project_phid;
       }
     }
 
     if (!$phids) {
       return array();
     }
 
     return id(new PhabricatorHandleQuery())
       ->setViewer($viewer)
       ->withPHIDs($phids)
       ->execute();
   }
 
 }
diff --git a/src/applications/meta/controller/PhabricatorApplicationEditController.php b/src/applications/meta/controller/PhabricatorApplicationEditController.php
index dffb46c6a..ba82b3005 100644
--- a/src/applications/meta/controller/PhabricatorApplicationEditController.php
+++ b/src/applications/meta/controller/PhabricatorApplicationEditController.php
@@ -1,167 +1,187 @@
 <?php
 
 final class PhabricatorApplicationEditController
   extends PhabricatorApplicationsController {
 
   public function handleRequest(AphrontRequest $request) {
     $user = $request->getUser();
     $application = $request->getURIData('application');
 
     $application = id(new PhabricatorApplicationQuery())
       ->setViewer($user)
       ->withClasses(array($application))
       ->requireCapabilities(
         array(
           PhabricatorPolicyCapability::CAN_VIEW,
           PhabricatorPolicyCapability::CAN_EDIT,
         ))
       ->executeOne();
     if (!$application) {
       return new Aphront404Response();
     }
 
     $title = $application->getName();
 
     $view_uri = $this->getApplicationURI('view/'.get_class($application).'/');
 
     $policies = id(new PhabricatorPolicyQuery())
       ->setViewer($user)
       ->setObject($application)
       ->execute();
 
     if ($request->isFormPost()) {
       $result = array();
       foreach ($application->getCapabilities() as $capability) {
         $old = $application->getPolicy($capability);
         $new = $request->getStr('policy:'.$capability);
 
         if ($old == $new) {
           // No change to the setting.
           continue;
         }
 
         if (empty($policies[$new])) {
           // Not a standard policy, check for a custom policy.
           $policy = id(new PhabricatorPolicyQuery())
             ->setViewer($user)
             ->withPHIDs(array($new))
             ->executeOne();
           if (!$policy) {
             // Not a custom policy either. Can't set the policy to something
             // invalid, so skip this.
             continue;
           }
         }
 
         if ($new == PhabricatorPolicies::POLICY_PUBLIC) {
           $capobj = PhabricatorPolicyCapability::getCapabilityByKey(
             $capability);
           if (!$capobj || !$capobj->shouldAllowPublicPolicySetting()) {
             // Can't set non-public policies to public.
             continue;
           }
         }
 
         $result[$capability] = $new;
       }
 
       if ($result) {
         $key = 'phabricator.application-settings';
         $config_entry = PhabricatorConfigEntry::loadConfigEntry($key);
         $value = $config_entry->getValue();
 
         $phid = $application->getPHID();
         if (empty($value[$phid])) {
           $value[$application->getPHID()] = array();
         }
         if (empty($value[$phid]['policy'])) {
           $value[$phid]['policy'] = array();
         }
 
         $value[$phid]['policy'] = $result + $value[$phid]['policy'];
 
         // Don't allow users to make policy edits which would lock them out of
         // applications, since they would be unable to undo those actions.
         PhabricatorEnv::overrideConfig($key, $value);
         PhabricatorPolicyFilter::mustRetainCapability(
           $user,
           $application,
           PhabricatorPolicyCapability::CAN_VIEW);
 
         PhabricatorPolicyFilter::mustRetainCapability(
           $user,
           $application,
           PhabricatorPolicyCapability::CAN_EDIT);
 
         PhabricatorConfigEditor::storeNewValue(
           $user,
           $config_entry,
           $value,
           PhabricatorContentSource::newFromRequest($request));
       }
 
       return id(new AphrontRedirectResponse())->setURI($view_uri);
     }
 
     $descriptions = PhabricatorPolicyQuery::renderPolicyDescriptions(
       $user,
       $application);
 
     $form = id(new AphrontFormView())
       ->setUser($user);
 
     $locked_policies = PhabricatorEnv::getEnvConfig('policy.locked');
     foreach ($application->getCapabilities() as $capability) {
       $label = $application->getCapabilityLabel($capability);
       $can_edit = $application->isCapabilityEditable($capability);
       $locked = idx($locked_policies, $capability);
       $caption = $application->getCapabilityCaption($capability);
 
       if (!$can_edit || $locked) {
         $form->appendChild(
           id(new AphrontFormStaticControl())
             ->setLabel($label)
             ->setValue(idx($descriptions, $capability))
             ->setCaption($caption));
       } else {
-        $form->appendChild(
-          id(new AphrontFormPolicyControl())
+        $control = id(new AphrontFormPolicyControl())
           ->setUser($user)
           ->setDisabled($locked)
           ->setCapability($capability)
           ->setPolicyObject($application)
           ->setPolicies($policies)
           ->setLabel($label)
           ->setName('policy:'.$capability)
-          ->setCaption($caption));
+          ->setCaption($caption);
+
+        $template = $application->getCapabilityTemplatePHIDType($capability);
+        if ($template) {
+          $phid_types = PhabricatorPHIDType::getAllTypes();
+          $phid_type = idx($phid_types, $template);
+          if ($phid_type) {
+            $template_object = $phid_type->newObject();
+            if ($template_object) {
+              $template_policies = id(new PhabricatorPolicyQuery())
+                ->setViewer($user)
+                ->setObject($template_object)
+                ->execute();
+              $control->setPolicies($template_policies);
+              $control->setTemplateObject($template_object);
+            }
+          }
+
+          $control->setTemplatePHIDType($template);
+        }
+
+        $form->appendControl($control);
       }
 
     }
 
     $form->appendChild(
       id(new AphrontFormSubmitControl())
         ->setValue(pht('Save Policies'))
         ->addCancelButton($view_uri));
 
     $crumbs = $this->buildApplicationCrumbs();
     $crumbs->addTextCrumb($application->getName(), $view_uri);
     $crumbs->addTextCrumb(pht('Edit Policies'));
 
     $header = id(new PHUIHeaderView())
       ->setHeader(pht('Edit Policies: %s', $application->getName()));
 
     $object_box = id(new PHUIObjectBoxView())
       ->setHeader($header)
       ->setForm($form);
 
     return $this->buildApplicationPage(
       array(
         $crumbs,
         $object_box,
       ),
       array(
         'title' => $title,
       ));
   }
 
 }
diff --git a/src/applications/meta/panel/__tests__/PhabricatorApplicationConfigurationPanelTestCase.php b/src/applications/meta/panel/__tests__/PhabricatorApplicationConfigurationPanelTestCase.php
new file mode 100644
index 000000000..10aab2377
--- /dev/null
+++ b/src/applications/meta/panel/__tests__/PhabricatorApplicationConfigurationPanelTestCase.php
@@ -0,0 +1,11 @@
+<?php
+
+final class PhabricatorApplicationConfigurationPanelTestCase
+  extends PhabricatorTestCase {
+
+  public function testLoadAllPanels() {
+    PhabricatorApplicationConfigurationPanel::loadAllPanels();
+    $this->assertTrue(true);
+  }
+
+}
diff --git a/src/applications/metamta/adapter/PhabricatorMailImplementationAdapter.php b/src/applications/metamta/adapter/PhabricatorMailImplementationAdapter.php
index 8b7fd6016..336330190 100644
--- a/src/applications/metamta/adapter/PhabricatorMailImplementationAdapter.php
+++ b/src/applications/metamta/adapter/PhabricatorMailImplementationAdapter.php
@@ -1,35 +1,35 @@
 <?php
 
-abstract class PhabricatorMailImplementationAdapter {
+abstract class PhabricatorMailImplementationAdapter extends Phobject {
 
   abstract public function setFrom($email, $name = '');
   abstract public function addReplyTo($email, $name = '');
   abstract public function addTos(array $emails);
   abstract public function addCCs(array $emails);
   abstract public function addAttachment($data, $filename, $mimetype);
   abstract public function addHeader($header_name, $header_value);
   abstract public function setBody($plaintext_body);
   abstract public function setHTMLBody($html_body);
   abstract public function setSubject($subject);
 
   /**
    * Some mailers, notably Amazon SES, do not support us setting a specific
    * Message-ID header.
    */
   abstract public function supportsMessageIDHeader();
 
 
   /**
    * Send the message. Generally, this means connecting to some service and
    * handing data to it.
    *
    * If the adapter determines that the mail will never be deliverable, it
    * should throw a @{class:PhabricatorMetaMTAPermanentFailureException}.
    *
    * For temporary failures, throw some other exception or return `false`.
    *
    * @return bool True on success.
    */
   abstract public function send();
 
 }
diff --git a/src/applications/metamta/adapter/PhabricatorMailImplementationPHPMailerLiteAdapter.php b/src/applications/metamta/adapter/PhabricatorMailImplementationPHPMailerLiteAdapter.php
index e75883203..2a8b12b64 100644
--- a/src/applications/metamta/adapter/PhabricatorMailImplementationPHPMailerLiteAdapter.php
+++ b/src/applications/metamta/adapter/PhabricatorMailImplementationPHPMailerLiteAdapter.php
@@ -1,105 +1,107 @@
 <?php
 
 /**
  * TODO: Should be final, but inherited by SES.
  */
 class PhabricatorMailImplementationPHPMailerLiteAdapter
   extends PhabricatorMailImplementationAdapter {
 
+  protected $mailer;
+
   /**
    * @phutil-external-symbol class PHPMailerLite
    */
   public function __construct() {
     $root = phutil_get_library_root('phabricator');
     $root = dirname($root);
     require_once $root.'/externals/phpmailer/class.phpmailer-lite.php';
     $this->mailer = new PHPMailerLite($use_exceptions = true);
     $this->mailer->CharSet = 'utf-8';
 
     $encoding = PhabricatorEnv::getEnvConfig('phpmailer.smtp-encoding', '8bit');
     $this->mailer->Encoding = $encoding;
 
     // By default, PHPMailerLite sends one mail per recipient. We handle
     // multiplexing higher in the stack, so tell it to send mail exactly
     // like we ask.
     $this->mailer->SingleTo = false;
   }
 
   public function supportsMessageIDHeader() {
     return true;
   }
 
   public function setFrom($email, $name = '') {
     $this->mailer->SetFrom($email, $name, $crazy_side_effects = false);
     return $this;
   }
 
   public function addReplyTo($email, $name = '') {
     $this->mailer->AddReplyTo($email, $name);
     return $this;
   }
 
   public function addTos(array $emails) {
     foreach ($emails as $email) {
       $this->mailer->AddAddress($email);
     }
     return $this;
   }
 
   public function addCCs(array $emails) {
     foreach ($emails as $email) {
       $this->mailer->AddCC($email);
     }
     return $this;
   }
 
   public function addAttachment($data, $filename, $mimetype) {
     $this->mailer->AddStringAttachment(
       $data,
       $filename,
       'base64',
       $mimetype);
     return $this;
   }
 
   public function addHeader($header_name, $header_value) {
     if (strtolower($header_name) == 'message-id') {
       $this->mailer->MessageID = $header_value;
     } else {
       $this->mailer->AddCustomHeader($header_name.': '.$header_value);
     }
     return $this;
   }
 
   public function setBody($body) {
     $this->mailer->Body = $body;
     $this->mailer->IsHTML(false);
     return $this;
   }
 
 
   /**
    * Note: phpmailer-lite does NOT support sending messages with mixed version
    * (plaintext and html). So for now lets just use HTML if it's available.
    * @param $html
    */
   public function setHTMLBody($html_body) {
     $this->mailer->Body = $html_body;
     $this->mailer->IsHTML(true);
     return $this;
   }
 
   public function setSubject($subject) {
     $this->mailer->Subject = $subject;
     return $this;
   }
 
   public function hasValidRecipients() {
     return true;
   }
 
   public function send() {
     return $this->mailer->Send();
   }
 
 }
diff --git a/src/applications/metamta/applicationpanel/PhabricatorMetaMTAApplicationEmailPanel.php b/src/applications/metamta/applicationpanel/PhabricatorMetaMTAApplicationEmailPanel.php
index ac8a3d560..ba0360307 100644
--- a/src/applications/metamta/applicationpanel/PhabricatorMetaMTAApplicationEmailPanel.php
+++ b/src/applications/metamta/applicationpanel/PhabricatorMetaMTAApplicationEmailPanel.php
@@ -1,384 +1,407 @@
 <?php
 
 final class PhabricatorMetaMTAApplicationEmailPanel
   extends PhabricatorApplicationConfigurationPanel {
 
   public function getPanelKey() {
     return 'email';
   }
 
   public function shouldShowForApplication(
     PhabricatorApplication $application) {
     return $application->supportsEmailIntegration();
   }
 
   public function buildConfigurationPagePanel() {
     $viewer = $this->getViewer();
     $application = $this->getApplication();
 
-    $addresses = id(new PhabricatorMetaMTAApplicationEmailQuery())
-      ->setViewer($viewer)
-      ->withApplicationPHIDs(array($application->getPHID()))
-      ->execute();
-
-    $rows = array();
-    foreach ($addresses as $address) {
-      $rows[] = array(
-        $address->getAddress(),
-      );
-    }
-
-    $table = id(new AphrontTableView($rows))
-      ->setNoDataString(pht('No email addresses configured.'))
-      ->setHeaders(
-        array(
-          pht('Address'),
-        ));
+    $table = $this->buildEmailTable($is_edit = false, null);
 
     $can_edit = PhabricatorPolicyFilter::hasCapability(
       $viewer,
       $application,
       PhabricatorPolicyCapability::CAN_EDIT);
 
     $header = id(new PHUIHeaderView())
       ->setHeader(pht('Application Emails'))
       ->addActionLink(
         id(new PHUIButtonView())
           ->setTag('a')
           ->setText(pht('Edit Application Emails'))
           ->setIcon(
             id(new PHUIIconView())
               ->setIconFont('fa-pencil'))
           ->setHref($this->getPanelURI())
           ->setDisabled(!$can_edit)
           ->setWorkflow(!$can_edit));
 
 
     $box = id(new PHUIObjectBoxView())
       ->setHeader($header)
       ->setTable($table);
 
     return $box;
   }
 
   public function handlePanelRequest(
     AphrontRequest $request,
     PhabricatorController $controller) {
     $viewer = $request->getViewer();
     $application = $this->getApplication();
 
     $path = $request->getURIData('path');
     if (strlen($path)) {
       return new Aphront404Response();
     }
 
     $uri = $request->getRequestURI();
     $uri->setQueryParams(array());
 
     $new = $request->getStr('new');
     $edit = $request->getInt('edit');
     $delete = $request->getInt('delete');
 
     if ($new) {
       return $this->returnNewAddressResponse($request, $uri, $application);
     }
 
     if ($edit) {
       return $this->returnEditAddressResponse($request, $uri, $edit);
     }
 
     if ($delete) {
       return $this->returnDeleteAddressResponse($request, $uri, $delete);
     }
 
-    $emails = id(new PhabricatorMetaMTAApplicationEmailQuery())
-      ->setViewer($viewer)
-      ->withApplicationPHIDs(array($application->getPHID()))
-      ->execute();
-
-    $highlight = $request->getInt('highlight');
-    $rowc = array();
-    $rows = array();
-    foreach ($emails as $email) {
+    $table = $this->buildEmailTable(
+      $is_edit = true,
+      $request->getInt('id'));
 
-      $button_edit = javelin_tag(
-        'a',
-        array(
-          'class' => 'button small grey',
-          'href'  => $uri->alter('edit', $email->getID()),
-          'sigil' => 'workflow',
-        ),
-        pht('Edit'));
-
-      $button_remove = javelin_tag(
-        'a',
-        array(
-          'class'   => 'button small grey',
-          'href'    => $uri->alter('delete', $email->getID()),
-          'sigil'   => 'workflow',
-        ),
-        pht('Delete'));
-
-      if ($highlight == $email->getID()) {
-        $rowc[] = 'highlighted';
-      } else {
-        $rowc[] = null;
-      }
-
-      $rows[] = array(
-        $email->getAddress(),
-        $button_edit,
-        $button_remove,
-      );
-    }
-
-    $table = id(new AphrontTableView($rows))
-      ->setNoDataString(pht('No application emails created yet.'));
-    $table->setHeaders(
-      array(
-        pht('Email'),
-        pht('Edit'),
-        pht('Delete'),
-      ));
-    $table->setColumnClasses(
-      array(
-        'wide',
-        'action',
-        'action',
-      ));
-    $table->setRowClasses($rowc);
-    $table->setColumnVisibility(
-      array(
-        true,
-        true,
-        true,
-      ));
     $form = id(new AphrontFormView())
       ->setUser($viewer);
 
     $crumbs = $controller->buildPanelCrumbs($this);
     $crumbs->addTextCrumb(pht('Edit Application Emails'));
 
     $header = id(new PHUIHeaderView())
       ->setHeader(pht('Edit Application Emails: %s', $application->getName()))
       ->setSubheader($application->getAppEmailBlurb());
 
     $icon = id(new PHUIIconView())
       ->setIconFont('fa-plus');
     $button = new PHUIButtonView();
     $button->setText(pht('Add New Address'));
     $button->setTag('a');
     $button->setHref($uri->alter('new', 'true'));
     $button->setIcon($icon);
     $button->addSigil('workflow');
     $header->addActionLink($button);
 
     $object_box = id(new PHUIObjectBoxView())
       ->setHeader($header)
       ->setTable($table);
 
     $title = $application->getName();
 
     return $controller->buildPanelPage(
       $this,
       array(
         $crumbs,
         $object_box,
       ),
       array(
         'title' => $title,
       ));
   }
 
-  private function validateApplicationEmail($email) {
-    $errors = array();
-    $e_email = true;
-
-    if (!strlen($email)) {
-      $e_email = pht('Required');
-      $errors[] = pht('Email is required.');
-    } else if (!PhabricatorUserEmail::isValidAddress($email)) {
-      $e_email = pht('Invalid');
-      $errors[] = PhabricatorUserEmail::describeValidAddresses();
-    } else if (!PhabricatorUserEmail::isAllowedAddress($email)) {
-      $e_email = pht('Disallowed');
-      $errors[] = PhabricatorUserEmail::describeAllowedAddresses();
-    }
-    $user_emails = id(new PhabricatorUserEmail())
-      ->loadAllWhere('address = %s', $email);
-    if ($user_emails) {
-      $e_email = pht('Duplicate');
-      $errors[] = pht('A user already has this email.');
-    }
-
-    return array($e_email, $errors);
-  }
-
   private function returnNewAddressResponse(
     AphrontRequest $request,
     PhutilURI $uri,
     PhabricatorApplication $application) {
 
     $viewer = $request->getUser();
     $email_object =
       PhabricatorMetaMTAApplicationEmail::initializeNewAppEmail($viewer)
       ->setApplicationPHID($application->getPHID());
 
     return $this->returnSaveAddressResponse(
       $request,
       $uri,
       $email_object,
       $is_new = true);
   }
 
   private function returnEditAddressResponse(
     AphrontRequest $request,
     PhutilURI $uri,
     $email_object_id) {
 
     $viewer = $request->getUser();
     $email_object = id(new PhabricatorMetaMTAApplicationEmailQuery())
       ->setViewer($viewer)
       ->withIDs(array($email_object_id))
       ->requireCapabilities(
         array(
           PhabricatorPolicyCapability::CAN_VIEW,
           PhabricatorPolicyCapability::CAN_EDIT,
         ))
       ->executeOne();
     if (!$email_object) {
       return new Aphront404Response();
     }
 
     return $this->returnSaveAddressResponse(
       $request,
       $uri,
       $email_object,
       $is_new = false);
   }
 
   private function returnSaveAddressResponse(
     AphrontRequest $request,
     PhutilURI $uri,
     PhabricatorMetaMTAApplicationEmail $email_object,
     $is_new) {
 
     $viewer = $request->getUser();
 
-    $e_email = true;
-    $email   = null;
-    $errors  = array();
-    $default_user_key =
+    $config_default =
       PhabricatorMetaMTAApplicationEmail::CONFIG_DEFAULT_AUTHOR;
-    if ($request->isDialogFormPost()) {
-      $email = trim($request->getStr('email'));
-      list($e_email, $errors) = $this->validateApplicationEmail($email);
-      $email_object->setAddress($email);
-      $default_user = $request->getArr($default_user_key);
-      $default_user = reset($default_user);
-      if ($default_user) {
-        $email_object->setConfigValue($default_user_key, $default_user);
-      }
 
-      if (!$errors) {
-        try {
-          $email_object->save();
-          return id(new AphrontRedirectResponse())->setURI(
-            $uri->alter('highlight', $email_object->getID()));
-        } catch (AphrontDuplicateKeyQueryException $ex) {
-          $e_email = pht('Duplicate');
-          $errors[] = pht(
-            'Another application is already configured to use this email '.
-            'address.');
-        }
-      }
-    }
+    $e_email = true;
+    $v_email = $email_object->getAddress();
+    $e_space = null;
+    $v_space = $email_object->getSpacePHID();
+    $v_default = $email_object->getConfigValue($config_default);
 
-    if ($errors) {
-      $errors = id(new PHUIInfoView())
-        ->setErrors($errors);
+    $validation_exception = null;
+    if ($request->isDialogFormPost()) {
+      $e_email = null;
+
+      $v_email = trim($request->getStr('email'));
+      $v_space = $request->getStr('spacePHID');
+      $v_default = $request->getArr($config_default);
+      $v_default = nonempty(head($v_default), null);
+
+      $type_address =
+        PhabricatorMetaMTAApplicationEmailTransaction::TYPE_ADDRESS;
+      $type_space = PhabricatorTransactions::TYPE_SPACE;
+      $type_config =
+        PhabricatorMetaMTAApplicationEmailTransaction::TYPE_CONFIG;
+
+      $key_config = PhabricatorMetaMTAApplicationEmailTransaction::KEY_CONFIG;
+
+      $xactions = array();
+
+      $xactions[] = id(new PhabricatorMetaMTAApplicationEmailTransaction())
+        ->setTransactionType($type_address)
+        ->setNewValue($v_email);
+
+      $xactions[] = id(new PhabricatorMetaMTAApplicationEmailTransaction())
+        ->setTransactionType($type_space)
+        ->setNewValue($v_space);
+
+      $xactions[] = id(new PhabricatorMetaMTAApplicationEmailTransaction())
+        ->setTransactionType($type_config)
+        ->setMetadataValue($key_config, $config_default)
+        ->setNewValue($v_default);
+
+      $editor = id(new PhabricatorMetaMTAApplicationEmailEditor())
+        ->setActor($viewer)
+        ->setContentSourceFromRequest($request)
+        ->setContinueOnNoEffect(true);
+
+      try {
+        $editor->applyTransactions($email_object, $xactions);
+
+        return id(new AphrontRedirectResponse())->setURI(
+          $uri->alter('highlight', $email_object->getID()));
+      } catch (PhabricatorApplicationTransactionValidationException $ex) {
+        $validation_exception = $ex;
+        $e_email = $ex->getShortMessage($type_address);
+        $e_space = $ex->getShortMessage($type_space);
+      }
     }
 
-    $default_user = $email_object->getConfigValue($default_user_key);
-    if ($default_user) {
-      $default_user_value = array($default_user);
+    if ($v_default) {
+      $v_default = array($v_default);
     } else {
-      $default_user_value = array();
+      $v_default = array();
     }
 
     $form = id(new AphrontFormView())
       ->setUser($viewer)
       ->appendChild(
         id(new AphrontFormTextControl())
           ->setLabel(pht('Email'))
           ->setName('email')
-          ->setValue($email_object->getAddress())
-          ->setCaption(PhabricatorUserEmail::describeAllowedAddresses())
-          ->setError($e_email))
+          ->setValue($v_email)
+          ->setError($e_email));
+
+    if (PhabricatorSpacesNamespaceQuery::getViewerSpacesExist($viewer)) {
+      $form->appendControl(
+        id(new AphrontFormSelectControl())
+          ->setLabel(pht('Space'))
+          ->setName('spacePHID')
+          ->setValue($v_space)
+          ->setError($e_space)
+          ->setOptions(
+            PhabricatorSpacesNamespaceQuery::getSpaceOptionsForViewer(
+              $viewer,
+              $v_space)));
+    }
+
+    $form
       ->appendControl(
         id(new AphrontFormTokenizerControl())
           ->setDatasource(new PhabricatorPeopleDatasource())
           ->setLabel(pht('Default Author'))
-          ->setName($default_user_key)
+          ->setName($config_default)
           ->setLimit(1)
-          ->setValue($default_user_value)
+          ->setValue($v_default)
           ->setCaption(pht(
             'Used if the "From:" address does not map to a known account.')));
+
     if ($is_new) {
       $title = pht('New Address');
     } else {
       $title = pht('Edit Address');
     }
+
     $dialog = id(new AphrontDialogView())
       ->setUser($viewer)
       ->setWidth(AphrontDialogView::WIDTH_FORM)
       ->setTitle($title)
-      ->appendChild($errors)
+      ->setValidationException($validation_exception)
       ->appendForm($form)
       ->addSubmitButton(pht('Save'))
       ->addCancelButton($uri);
 
     if ($is_new) {
       $dialog->addHiddenInput('new', 'true');
     }
 
     return id(new AphrontDialogResponse())->setDialog($dialog);
   }
 
   private function returnDeleteAddressResponse(
     AphrontRequest $request,
     PhutilURI $uri,
     $email_object_id) {
 
-    $viewer = $request->getUser();
+    $viewer = $this->getViewer();
+
     $email_object = id(new PhabricatorMetaMTAApplicationEmailQuery())
       ->setViewer($viewer)
       ->withIDs(array($email_object_id))
       ->requireCapabilities(
         array(
           PhabricatorPolicyCapability::CAN_VIEW,
           PhabricatorPolicyCapability::CAN_EDIT,
         ))
       ->executeOne();
     if (!$email_object) {
       return new Aphront404Response();
     }
 
     if ($request->isDialogFormPost()) {
-      $email_object->delete();
+      $engine = new PhabricatorDestructionEngine();
+      $engine->destroyObject($email_object);
       return id(new AphrontRedirectResponse())->setURI($uri);
     }
 
     $dialog = id(new AphrontDialogView())
       ->setUser($viewer)
       ->addHiddenInput('delete', $email_object_id)
       ->setTitle(pht('Delete Address'))
       ->appendParagraph(pht(
         'Are you sure you want to delete this email address?'))
       ->addSubmitButton(pht('Delete'))
       ->addCancelButton($uri);
 
     return id(new AphrontDialogResponse())->setDialog($dialog);
   }
 
+  private function buildEmailTable($is_edit, $highlight) {
+    $viewer = $this->getViewer();
+    $application = $this->getApplication();
+    $uri = new PhutilURI($this->getPanelURI());
+
+    $emails = id(new PhabricatorMetaMTAApplicationEmailQuery())
+      ->setViewer($viewer)
+      ->withApplicationPHIDs(array($application->getPHID()))
+      ->execute();
+
+    $rowc = array();
+    $rows = array();
+    foreach ($emails as $email) {
+
+      $button_edit = javelin_tag(
+        'a',
+        array(
+          'class' => 'button small grey',
+          'href'  => $uri->alter('edit', $email->getID()),
+          'sigil' => 'workflow',
+        ),
+        pht('Edit'));
+
+      $button_remove = javelin_tag(
+        'a',
+        array(
+          'class'   => 'button small grey',
+          'href'    => $uri->alter('delete', $email->getID()),
+          'sigil'   => 'workflow',
+        ),
+        pht('Delete'));
+
+      if ($highlight == $email->getID()) {
+        $rowc[] = 'highlighted';
+      } else {
+        $rowc[] = null;
+      }
+
+      $space_phid = PhabricatorSpacesNamespaceQuery::getObjectSpacePHID($email);
+      if ($space_phid) {
+        $email_space = $viewer->renderHandle($space_phid);
+      } else {
+        $email_space = null;
+      }
+
+      $rows[] = array(
+        $email_space,
+        $email->getAddress(),
+        $button_edit,
+        $button_remove,
+      );
+    }
+
+    $table = id(new AphrontTableView($rows))
+      ->setNoDataString(pht('No application emails created yet.'));
+    $table->setHeaders(
+      array(
+        pht('Space'),
+        pht('Email'),
+        pht('Edit'),
+        pht('Delete'),
+      ));
+    $table->setColumnClasses(
+      array(
+        '',
+        'wide',
+        'action',
+        'action',
+      ));
+    $table->setRowClasses($rowc);
+    $table->setColumnVisibility(
+      array(
+        PhabricatorSpacesNamespaceQuery::getViewerSpacesExist($viewer),
+        true,
+        $is_edit,
+        $is_edit,
+      ));
+
+    return $table;
+  }
+
 }
diff --git a/src/applications/metamta/command/__tests__/MetaMTAEmailTransactionCommandTestCase.php b/src/applications/metamta/command/__tests__/MetaMTAEmailTransactionCommandTestCase.php
new file mode 100644
index 000000000..3668594ca
--- /dev/null
+++ b/src/applications/metamta/command/__tests__/MetaMTAEmailTransactionCommandTestCase.php
@@ -0,0 +1,10 @@
+<?php
+
+final class MetaMTAEmailTransactionCommandTestCase extends PhabricatorTestCase {
+
+  public function testGetAllTypes() {
+    MetaMTAEmailTransactionCommand::getAllCommands();
+    $this->assertTrue(true);
+  }
+
+}
diff --git a/src/applications/metamta/constants/MetaMTAConstants.php b/src/applications/metamta/constants/MetaMTAConstants.php
index 0e9e6939a..9b3fe7121 100644
--- a/src/applications/metamta/constants/MetaMTAConstants.php
+++ b/src/applications/metamta/constants/MetaMTAConstants.php
@@ -1,3 +1,3 @@
 <?php
 
-abstract class MetaMTAConstants {}
+abstract class MetaMTAConstants extends Phobject {}
diff --git a/src/applications/metamta/contentsource/PhabricatorContentSource.php b/src/applications/metamta/contentsource/PhabricatorContentSource.php
index e5a29572c..c291b2069 100644
--- a/src/applications/metamta/contentsource/PhabricatorContentSource.php
+++ b/src/applications/metamta/contentsource/PhabricatorContentSource.php
@@ -1,104 +1,104 @@
 <?php
 
-final class PhabricatorContentSource {
+final class PhabricatorContentSource extends Phobject {
 
   const SOURCE_UNKNOWN  = 'unknown';
   const SOURCE_WEB      = 'web';
   const SOURCE_EMAIL    = 'email';
   const SOURCE_CONDUIT  = 'conduit';
   const SOURCE_MOBILE   = 'mobile';
   const SOURCE_TABLET   = 'tablet';
   const SOURCE_FAX      = 'fax';
   const SOURCE_CONSOLE  = 'console';
   const SOURCE_HERALD   = 'herald';
   const SOURCE_LEGACY   = 'legacy';
   const SOURCE_DAEMON   = 'daemon';
   const SOURCE_LIPSUM   = 'lipsum';
   const SOURCE_PHORTUNE = 'phortune';
 
   private $source;
   private $params = array();
 
   private function __construct() {
     // <empty>
   }
 
   public static function newForSource($source, array $params) {
     $obj = new PhabricatorContentSource();
     $obj->source = $source;
     $obj->params = $params;
 
     return $obj;
   }
 
   public static function newFromSerialized($serialized) {
     $dict = json_decode($serialized, true);
     if (!is_array($dict)) {
       $dict = array();
     }
 
     $obj = new PhabricatorContentSource();
     $obj->source = idx($dict, 'source', self::SOURCE_UNKNOWN);
     $obj->params = idx($dict, 'params', array());
 
     return $obj;
   }
 
   public static function newConsoleSource() {
     return self::newForSource(
       self::SOURCE_CONSOLE,
       array());
   }
 
   public static function newFromRequest(AphrontRequest $request) {
     return self::newForSource(
       self::SOURCE_WEB,
       array(
         'ip' => $request->getRemoteAddr(),
       ));
   }
 
   public static function newFromConduitRequest(ConduitAPIRequest $request) {
     return self::newForSource(
       self::SOURCE_CONDUIT,
       array());
   }
 
   public static function getSourceNameMap() {
     return array(
       self::SOURCE_WEB      => pht('Web'),
       self::SOURCE_EMAIL    => pht('Email'),
       self::SOURCE_CONDUIT  => pht('Conduit'),
       self::SOURCE_MOBILE   => pht('Mobile'),
       self::SOURCE_TABLET   => pht('Tablet'),
       self::SOURCE_FAX      => pht('Fax'),
       self::SOURCE_CONSOLE  => pht('Console'),
       self::SOURCE_LEGACY   => pht('Legacy'),
       self::SOURCE_HERALD   => pht('Herald'),
       self::SOURCE_DAEMON   => pht('Daemons'),
       self::SOURCE_LIPSUM   => pht('Lipsum'),
       self::SOURCE_UNKNOWN  => pht('Old World'),
       self::SOURCE_PHORTUNE => pht('Phortune'),
     );
   }
 
   public function serialize() {
     return json_encode(array(
       'source' => $this->getSource(),
       'params' => $this->getParams(),
     ));
   }
 
   public function getSource() {
     return $this->source;
   }
 
   public function getParams() {
     return $this->params;
   }
 
   public function getParam($key, $default = null) {
     return idx($this->params, $key, $default);
   }
 
 }
diff --git a/src/applications/metamta/editor/PhabricatorMetaMTAApplicationEmailEditor.php b/src/applications/metamta/editor/PhabricatorMetaMTAApplicationEmailEditor.php
new file mode 100644
index 000000000..2cbd164a8
--- /dev/null
+++ b/src/applications/metamta/editor/PhabricatorMetaMTAApplicationEmailEditor.php
@@ -0,0 +1,145 @@
+<?php
+
+final class PhabricatorMetaMTAApplicationEmailEditor
+  extends PhabricatorApplicationTransactionEditor {
+
+  public function getEditorApplicationClass() {
+    return pht('PhabricatorMetaMTAApplication');
+  }
+
+  public function getEditorObjectsDescription() {
+    return pht('Application Emails');
+  }
+
+  public function getTransactionTypes() {
+    $types = parent::getTransactionTypes();
+
+    $types[] = PhabricatorMetaMTAApplicationEmailTransaction::TYPE_ADDRESS;
+    $types[] = PhabricatorMetaMTAApplicationEmailTransaction::TYPE_CONFIG;
+
+    return $types;
+  }
+
+  protected function getCustomTransactionOldValue(
+    PhabricatorLiskDAO $object,
+    PhabricatorApplicationTransaction $xaction) {
+
+    switch ($xaction->getTransactionType()) {
+      case PhabricatorMetaMTAApplicationEmailTransaction::TYPE_ADDRESS:
+        return $object->getAddress();
+      case PhabricatorMetaMTAApplicationEmailTransaction::TYPE_CONFIG:
+        $key = $xaction->getMetadataValue(
+          PhabricatorMetaMTAApplicationEmailTransaction::KEY_CONFIG);
+        return $object->getConfigValue($key);
+    }
+
+    return parent::getCustomTransactionOldValue($object, $xaction);
+  }
+
+  protected function getCustomTransactionNewValue(
+    PhabricatorLiskDAO $object,
+    PhabricatorApplicationTransaction $xaction) {
+
+    switch ($xaction->getTransactionType()) {
+      case PhabricatorMetaMTAApplicationEmailTransaction::TYPE_ADDRESS:
+      case PhabricatorMetaMTAApplicationEmailTransaction::TYPE_CONFIG:
+        return $xaction->getNewValue();
+    }
+
+    return parent::getCustomTransactionNewValue($object, $xaction);
+  }
+
+  protected function applyCustomInternalTransaction(
+    PhabricatorLiskDAO $object,
+    PhabricatorApplicationTransaction $xaction) {
+
+    $new = $xaction->getNewValue();
+
+    switch ($xaction->getTransactionType()) {
+      case PhabricatorMetaMTAApplicationEmailTransaction::TYPE_ADDRESS:
+        $object->setAddress($new);
+        return;
+      case PhabricatorMetaMTAApplicationEmailTransaction::TYPE_CONFIG:
+        $key = $xaction->getMetadataValue(
+          PhabricatorMetaMTAApplicationEmailTransaction::KEY_CONFIG);
+        $object->setConfigValue($key, $new);
+        return;
+    }
+
+    return parent::applyCustomInternalTransaction($object, $xaction);
+  }
+
+  protected function applyCustomExternalTransaction(
+    PhabricatorLiskDAO $object,
+    PhabricatorApplicationTransaction $xaction) {
+
+    switch ($xaction->getTransactionType()) {
+      case PhabricatorMetaMTAApplicationEmailTransaction::TYPE_ADDRESS:
+      case PhabricatorMetaMTAApplicationEmailTransaction::TYPE_CONFIG:
+        return;
+    }
+
+    return parent::applyCustomExternalTransaction($object, $xaction);
+  }
+
+  protected function validateTransaction(
+    PhabricatorLiskDAO $object,
+    $type,
+    array $xactions) {
+
+    $errors = parent::validateTransaction($object, $type, $xactions);
+
+    switch ($type) {
+      case PhabricatorMetaMTAApplicationEmailTransaction::TYPE_ADDRESS:
+        foreach ($xactions as $xaction) {
+          $email = $xaction->getNewValue();
+          if (!strlen($email)) {
+            // We'll deal with this below.
+            continue;
+          }
+
+          if (!PhabricatorUserEmail::isValidAddress($email)) {
+            $errors[] = new PhabricatorApplicationTransactionValidationError(
+              $type,
+              pht('Invalid'),
+              pht('Email address is not formatted properly.'));
+          }
+        }
+
+        $missing = $this->validateIsEmptyTextField(
+          $object->getAddress(),
+          $xactions);
+
+        if ($missing) {
+          $error = new PhabricatorApplicationTransactionValidationError(
+            $type,
+            pht('Required'),
+            pht('You must provide an email address.'),
+            nonempty(last($xactions), null));
+
+          $error->setIsMissingFieldError(true);
+          $errors[] = $error;
+        }
+        break;
+    }
+
+    return $errors;
+  }
+
+  protected function didCatchDuplicateKeyException(
+    PhabricatorLiskDAO $object,
+    array $xactions,
+    Exception $ex) {
+
+    $errors = array();
+    $errors[] = new PhabricatorApplicationTransactionValidationError(
+      PhabricatorMetaMTAApplicationEmailTransaction::TYPE_ADDRESS,
+      pht('Duplicate'),
+      pht('This email address is already in use.'),
+      null);
+
+    throw new PhabricatorApplicationTransactionValidationException($errors);
+  }
+
+
+}
diff --git a/src/applications/metamta/parser/PhabricatorMetaMTAEmailBodyParser.php b/src/applications/metamta/parser/PhabricatorMetaMTAEmailBodyParser.php
index 07770600e..be0401f06 100644
--- a/src/applications/metamta/parser/PhabricatorMetaMTAEmailBodyParser.php
+++ b/src/applications/metamta/parser/PhabricatorMetaMTAEmailBodyParser.php
@@ -1,167 +1,167 @@
 <?php
 
-final class PhabricatorMetaMTAEmailBodyParser {
+final class PhabricatorMetaMTAEmailBodyParser extends Phobject {
 
   /**
    * Mails can have bodies such as
    *
    *   !claim
    *
    *   taking this task
    *
    * Or
    *
    *   !assign epriestley
    *
    *   please, take this task I took; its hard
    *
    * This function parses such an email body and returns a dictionary
    * containing a clean body text (e.g. "taking this task"), and a list of
    * commands. For example, this body above might parse as:
    *
    *   array(
    *     'body' => 'please, take this task I took; its hard',
    *     'commands' => array(
    *       array('assign', 'epriestley'),
    *     ),
    *   )
    *
    * @param   string  Raw mail text body.
    * @return  dict    Parsed body.
    */
   public function parseBody($body) {
     $body = $this->stripTextBody($body);
 
     $commands = array();
 
     $lines = phutil_split_lines($body, $retain_endings = true);
 
     // We'll match commands at the beginning and end of the mail, but not
     // in the middle of the mail body.
     list($top_commands, $lines) = $this->stripCommands($lines);
     list($end_commands, $lines) = $this->stripCommands(array_reverse($lines));
     $lines = array_reverse($lines);
     $commands = array_merge($top_commands, array_reverse($end_commands));
 
     $lines = rtrim(implode('', $lines));
 
     return array(
       'body' => $lines,
       'commands' => $commands,
     );
   }
 
   private function stripCommands(array $lines) {
     $saw_command = false;
     $commands = array();
     foreach ($lines as $key => $line) {
       if (!strlen(trim($line)) && $saw_command) {
         unset($lines[$key]);
         continue;
       }
 
       $matches = null;
       if (!preg_match('/^\s*!(\w+.*$)/', $line, $matches)) {
         break;
       }
 
       $arg_str = $matches[1];
       $argv = preg_split('/[,\s]+/', trim($arg_str));
       $commands[] = $argv;
       unset($lines[$key]);
 
       $saw_command = true;
     }
 
     return array($commands, $lines);
   }
 
   public function stripTextBody($body) {
     return trim($this->stripSignature($this->stripQuotedText($body)));
   }
 
   private function stripQuotedText($body) {
 
     // Look for "On <date>, <user> wrote:". This may be split across multiple
     // lines. We need to be careful not to remove all of a message like this:
     //
     //   On which day do you want to meet?
     //
     //   On <date>, <user> wrote:
     //   > Let's set up a meeting.
 
     $start = null;
     $lines = phutil_split_lines($body);
     foreach ($lines as $key => $line) {
       if (preg_match('/^\s*>?\s*On\b/', $line)) {
         $start = $key;
       }
       if ($start !== null) {
         if (preg_match('/\bwrote:/', $line)) {
           $lines = array_slice($lines, 0, $start);
           $body = implode('', $lines);
           break;
         }
       }
     }
 
     // Outlook english
     $body = preg_replace(
       '/^\s*(> )?-----Original Message-----.*?/imsU',
       '',
       $body);
 
     // Outlook danish
     $body = preg_replace(
       '/^\s*(> )?-----Oprindelig Meddelelse-----.*?/imsU',
       '',
       $body);
 
     // See example in T3217.
     $body = preg_replace(
       '/^________________________________________\s+From:.*?/imsU',
       '',
       $body);
 
     // French GMail quoted text. See T8199.
     $body = preg_replace(
       '/^\s*\d{4}-\d{2}-\d{2} \d+:\d+ GMT.*:.*?/imsU',
       '',
       $body);
 
     return rtrim($body);
   }
 
   private function stripSignature($body) {
     // Quasi-"standard" delimiter, for lols see:
     //   https://bugzilla.mozilla.org/show_bug.cgi?id=58406
     $body = preg_replace(
       '/^-- +$.*/sm',
       '',
       $body);
 
     // Mailbox seems to make an attempt to comply with the "standard" but
     // omits the leading newline and uses an em dash. This may or may not have
     // the trailing space, but it's unique enough that there's no real ambiguity
     // in detecting it.
     $body = preg_replace(
       "/\s*\xE2\x80\x94\s*\nSent from Mailbox\s*\z/su",
       '',
       $body);
 
     // HTC Mail application (mobile)
     $body = preg_replace(
       '/^\s*^Sent from my HTC smartphone.*/sm',
       '',
       $body);
 
     // Apple iPhone
     $body = preg_replace(
       '/^\s*^Sent from my iPhone\s*$.*/sm',
       '',
       $body);
 
     return rtrim($body);
   }
 
 }
diff --git a/src/applications/metamta/query/PhabricatorMetaMTAActor.php b/src/applications/metamta/query/PhabricatorMetaMTAActor.php
index 81012d210..281e7fb90 100644
--- a/src/applications/metamta/query/PhabricatorMetaMTAActor.php
+++ b/src/applications/metamta/query/PhabricatorMetaMTAActor.php
@@ -1,121 +1,121 @@
 <?php
 
-final class PhabricatorMetaMTAActor {
+final class PhabricatorMetaMTAActor extends Phobject {
 
   const STATUS_DELIVERABLE = 'deliverable';
   const STATUS_UNDELIVERABLE = 'undeliverable';
 
   const REASON_UNLOADABLE = 'unloadable';
   const REASON_UNMAILABLE = 'unmailable';
   const REASON_NO_ADDRESS = 'noaddress';
   const REASON_DISABLED = 'disabled';
   const REASON_MAIL_DISABLED = 'maildisabled';
   const REASON_EXTERNAL_TYPE = 'exernaltype';
   const REASON_RESPONSE = 'response';
   const REASON_SELF = 'self';
   const REASON_MAILTAGS = 'mailtags';
   const REASON_BOT = 'bot';
   const REASON_FORCE = 'force';
   const REASON_FORCE_HERALD = 'force-herald';
 
   private $phid;
   private $emailAddress;
   private $name;
   private $status = self::STATUS_DELIVERABLE;
   private $reasons = array();
 
   public function setName($name) {
     $this->name = $name;
     return $this;
   }
 
   public function getName() {
     return $this->name;
   }
 
   public function setEmailAddress($email_address) {
     $this->emailAddress = $email_address;
     return $this;
   }
 
   public function getEmailAddress() {
     return $this->emailAddress;
   }
 
   public function setPHID($phid) {
     $this->phid = $phid;
     return $this;
   }
 
   public function getPHID() {
     return $this->phid;
   }
 
   public function setUndeliverable($reason) {
     $this->reasons[] = $reason;
     $this->status = self::STATUS_UNDELIVERABLE;
     return $this;
   }
 
   public function setDeliverable($reason) {
     $this->reasons[] = $reason;
     $this->status = self::STATUS_DELIVERABLE;
     return $this;
   }
 
   public function isDeliverable() {
     return ($this->status === self::STATUS_DELIVERABLE);
   }
 
   public function getDeliverabilityReasons() {
     return $this->reasons;
   }
 
   public static function getReasonDescription($reason) {
     $descriptions = array(
       self::REASON_DISABLED => pht(
         'This user is disabled; disabled users do not receive mail.'),
       self::REASON_BOT => pht(
         'This user is a bot; bot accounts do not receive mail.'),
       self::REASON_NO_ADDRESS => pht(
         'Unable to load an email address for this PHID.'),
       self::REASON_EXTERNAL_TYPE => pht(
         'Only external accounts of type "email" are deliverable; this '.
         'account has a different type.'),
       self::REASON_UNMAILABLE => pht(
         'This PHID type does not correspond to a mailable object.'),
       self::REASON_RESPONSE => pht(
         'This message is a response to another email message, and this '.
         'recipient received the original email message, so we are not '.
         'sending them this substantially similar message (for example, '.
         'the sender used "Reply All" instead of "Reply" in response to '.
         'mail from Phabricator).'),
       self::REASON_SELF => pht(
         'This recipient is the user whose actions caused delivery of '.
         'this message, but they have set preferences so they do not '.
         'receive mail about their own actions (Settings > Email '.
         'Preferences > Self Actions).'),
       self::REASON_MAIL_DISABLED => pht(
         'This recipient has disabled all email notifications '.
         '(Settings > Email Preferences > Email Notifications).'),
       self::REASON_MAILTAGS => pht(
         'This mail has tags which control which users receive it, and '.
         'this recipient has not elected to receive mail with any of '.
         'the tags on this message (Settings > Email Preferences).'),
       self::REASON_UNLOADABLE => pht(
         'Unable to load user record for this PHID.'),
       self::REASON_FORCE => pht(
         'Delivery of this mail is forced and ignores deliver preferences. '.
         'Mail which uses forced delivery is usually related to account '.
         'management or authentication. For example, password reset email '.
         'ignores mail preferences.'),
       self::REASON_FORCE_HERALD => pht(
         'This recipient was added by a "Send me an Email" rule in Herald, '.
         'which overrides some delivery settings.'),
     );
 
     return idx($descriptions, $reason, pht('Unknown Reason ("%s")', $reason));
   }
 
 
 }
diff --git a/src/applications/metamta/query/PhabricatorMetaMTAApplicationEmailQuery.php b/src/applications/metamta/query/PhabricatorMetaMTAApplicationEmailQuery.php
index b17322491..9c0c5d941 100644
--- a/src/applications/metamta/query/PhabricatorMetaMTAApplicationEmailQuery.php
+++ b/src/applications/metamta/query/PhabricatorMetaMTAApplicationEmailQuery.php
@@ -1,125 +1,111 @@
 <?php
 
 final class PhabricatorMetaMTAApplicationEmailQuery
   extends PhabricatorCursorPagedPolicyAwareQuery {
 
   private $ids;
   private $phids;
   private $addresses;
   private $addressPrefix;
   private $applicationPHIDs;
 
   public function withIDs(array $ids) {
     $this->ids = $ids;
     return $this;
   }
 
   public function withPHIDs(array $phids) {
     $this->phids = $phids;
     return $this;
   }
 
   public function withAddresses(array $addresses) {
     $this->addresses = $addresses;
     return $this;
   }
 
   public function withAddressPrefix($prefix) {
     $this->addressPrefix = $prefix;
     return $this;
   }
 
   public function withApplicationPHIDs(array $phids) {
     $this->applicationPHIDs = $phids;
     return $this;
   }
 
   protected function loadPage() {
-    $table  = new PhabricatorMetaMTAApplicationEmail();
-    $conn_r = $table->establishConnection('r');
-
-    $data = queryfx_all(
-      $conn_r,
-      'SELECT * FROM %T appemail %Q %Q %Q %Q',
-      $table->getTableName(),
-      $this->buildWhereClause($conn_r),
-      $this->buildApplicationSearchGroupClause($conn_r),
-      $this->buildOrderClause($conn_r),
-      $this->buildLimitClause($conn_r));
-
-    return $table->loadAllFromArray($data);
+    return $this->loadStandardPage(new PhabricatorMetaMTAApplicationEmail());
   }
 
   protected function willFilterPage(array $app_emails) {
     $app_emails_map = mgroup($app_emails, 'getApplicationPHID');
     $applications = id(new PhabricatorApplicationQuery())
       ->setViewer($this->getViewer())
       ->withPHIDs(array_keys($app_emails_map))
       ->execute();
     $applications = mpull($applications, null, 'getPHID');
 
     foreach ($app_emails_map as $app_phid => $app_emails_group) {
       foreach ($app_emails_group as $app_email) {
         $application = idx($applications, $app_phid);
         if (!$application) {
           unset($app_emails[$app_phid]);
           continue;
         }
         $app_email->attachApplication($application);
       }
     }
     return $app_emails;
   }
 
-  protected function buildWhereClause(AphrontDatabaseConnection $conn_r) {
-    $where = array();
+  protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) {
+    $where = parent::buildWhereClauseParts($conn);
 
     if ($this->addresses !== null) {
       $where[] = qsprintf(
-        $conn_r,
+        $conn,
         'appemail.address IN (%Ls)',
         $this->addresses);
     }
 
     if ($this->addressPrefix !== null) {
       $where[] = qsprintf(
-        $conn_r,
+        $conn,
         'appemail.address LIKE %>',
         $this->addressPrefix);
     }
 
     if ($this->applicationPHIDs !== null) {
       $where[] = qsprintf(
-        $conn_r,
+        $conn,
         'appemail.applicationPHID IN (%Ls)',
         $this->applicationPHIDs);
     }
 
     if ($this->phids !== null) {
       $where[] = qsprintf(
-        $conn_r,
+        $conn,
         'appemail.phid IN (%Ls)',
         $this->phids);
     }
 
     if ($this->ids !== null) {
       $where[] = qsprintf(
-        $conn_r,
+        $conn,
         'appemail.id IN (%Ld)',
         $this->ids);
     }
 
-    $where[] = $this->buildPagingClause($conn_r);
-
-    return $this->formatWhereClause($where);
+    return $where;
   }
 
   protected function getPrimaryTableAlias() {
     return 'appemail';
   }
 
   public function getQueryApplicationClass() {
     return 'PhabricatorMetaMTAApplication';
   }
 
 }
diff --git a/src/applications/metamta/query/PhabricatorMetaMTAApplicationEmailTransactionQuery.php b/src/applications/metamta/query/PhabricatorMetaMTAApplicationEmailTransactionQuery.php
new file mode 100644
index 000000000..4f4f6d11d
--- /dev/null
+++ b/src/applications/metamta/query/PhabricatorMetaMTAApplicationEmailTransactionQuery.php
@@ -0,0 +1,10 @@
+<?php
+
+final class PhabricatorMetaMTAApplicationEmailTransactionQuery
+  extends PhabricatorApplicationTransactionQuery {
+
+  public function getTemplateApplicationTransaction() {
+    return new PhabricatorMetaMTAApplicationEmailTransaction();
+  }
+
+}
diff --git a/src/applications/metamta/receiver/PhabricatorMailReceiver.php b/src/applications/metamta/receiver/PhabricatorMailReceiver.php
index b01a83f5a..07d364b21 100644
--- a/src/applications/metamta/receiver/PhabricatorMailReceiver.php
+++ b/src/applications/metamta/receiver/PhabricatorMailReceiver.php
@@ -1,271 +1,271 @@
 <?php
 
-abstract class PhabricatorMailReceiver {
+abstract class PhabricatorMailReceiver extends Phobject {
 
   private $applicationEmail;
 
   public function setApplicationEmail(
     PhabricatorMetaMTAApplicationEmail $email) {
     $this->applicationEmail = $email;
     return $this;
   }
 
   public function getApplicationEmail() {
     return $this->applicationEmail;
   }
 
   abstract public function isEnabled();
   abstract public function canAcceptMail(PhabricatorMetaMTAReceivedMail $mail);
   final protected function canAcceptApplicationMail(
     PhabricatorApplication $app,
     PhabricatorMetaMTAReceivedMail $mail) {
 
     $application_emails = id(new PhabricatorMetaMTAApplicationEmailQuery())
       ->setViewer($this->getViewer())
       ->withApplicationPHIDs(array($app->getPHID()))
       ->execute();
 
     foreach ($mail->getToAddresses() as $to_address) {
       foreach ($application_emails as $application_email) {
         $create_address = $application_email->getAddress();
         if ($this->matchAddresses($create_address, $to_address)) {
           $this->setApplicationEmail($application_email);
           return true;
         }
       }
     }
 
     return false;
   }
 
 
   abstract protected function processReceivedMail(
     PhabricatorMetaMTAReceivedMail $mail,
     PhabricatorUser $sender);
 
   final public function receiveMail(
     PhabricatorMetaMTAReceivedMail $mail,
     PhabricatorUser $sender) {
     $this->processReceivedMail($mail, $sender);
   }
 
   public function getViewer() {
     return PhabricatorUser::getOmnipotentUser();
   }
 
   public function validateSender(
     PhabricatorMetaMTAReceivedMail $mail,
     PhabricatorUser $sender) {
 
     $failure_reason = null;
     if ($sender->getIsDisabled()) {
       $failure_reason = pht(
         'Your account (%s) is disabled, so you can not interact with '.
         'Phabricator over email.',
         $sender->getUsername());
     } else if ($sender->getIsStandardUser()) {
       if (!$sender->getIsApproved()) {
         $failure_reason = pht(
           'Your account (%s) has not been approved yet. You can not interact '.
           'with Phabricator over email until your account is approved.',
           $sender->getUsername());
       } else if (PhabricatorUserEmail::isEmailVerificationRequired() &&
                !$sender->getIsEmailVerified()) {
         $failure_reason = pht(
           'You have not verified the email address for your account (%s). '.
           'You must verify your email address before you can interact '.
           'with Phabricator over email.',
           $sender->getUsername());
       }
     }
 
     if ($failure_reason) {
       throw new PhabricatorMetaMTAReceivedMailProcessingException(
         MetaMTAReceivedMailStatus::STATUS_DISABLED_SENDER,
         $failure_reason);
     }
   }
 
   /**
    * Identifies the sender's user account for a piece of received mail. Note
    * that this method does not validate that the sender is who they say they
    * are, just that they've presented some credential which corresponds to a
    * recognizable user.
    */
   public function loadSender(PhabricatorMetaMTAReceivedMail $mail) {
     $raw_from = $mail->getHeader('From');
     $from = self::getRawAddress($raw_from);
 
     $reasons = array();
 
     // Try to find a user with this email address.
     $user = PhabricatorUser::loadOneWithEmailAddress($from);
     if ($user) {
       return $user;
     } else {
       $reasons[] = pht(
         'This email was sent from "%s", but that address is not recognized by '.
         'Phabricator and does not correspond to any known user account.',
         $raw_from);
     }
 
     // If we missed on "From", try "Reply-To" if we're configured for it.
     $raw_reply_to = $mail->getHeader('Reply-To');
     if (strlen($raw_reply_to)) {
       $reply_to_key = 'metamta.insecure-auth-with-reply-to';
       $allow_reply_to = PhabricatorEnv::getEnvConfig($reply_to_key);
       if ($allow_reply_to) {
         $reply_to = self::getRawAddress($raw_reply_to);
 
         $user = PhabricatorUser::loadOneWithEmailAddress($reply_to);
         if ($user) {
           return $user;
         } else {
           $reasons[] = pht(
             'Phabricator is configured to authenticate users using the '.
             '"Reply-To" header, but the reply address ("%s") on this '.
             'message does not correspond to any known user account.',
             $raw_reply_to);
         }
       } else {
         $reasons[] = pht(
           '(Phabricator is not configured to authenticate users using the '.
           '"Reply-To" header, so it was ignored.)');
       }
     }
 
     // If we don't know who this user is, load or create an external user
     // account for them if we're configured for it.
     $email_key = 'phabricator.allow-email-users';
     $allow_email_users = PhabricatorEnv::getEnvConfig($email_key);
     if ($allow_email_users) {
       $from_obj = new PhutilEmailAddress($from);
       $xuser = id(new PhabricatorExternalAccountQuery())
         ->setViewer($this->getViewer())
         ->withAccountTypes(array('email'))
         ->withAccountDomains(array($from_obj->getDomainName(), 'self'))
         ->withAccountIDs(array($from_obj->getAddress()))
         ->requireCapabilities(
           array(
             PhabricatorPolicyCapability::CAN_VIEW,
             PhabricatorPolicyCapability::CAN_EDIT,
           ))
         ->loadOneOrCreate();
       return $xuser->getPhabricatorUser();
     } else {
       $reasons[] = pht(
         'Phabricator is also not configured to allow unknown external users '.
         'to send mail to the system using just an email address.');
       $reasons[] = pht(
         'To interact with Phabricator, add this address ("%s") to your '.
         'account.',
         $raw_from);
     }
 
     if ($this->getApplicationEmail()) {
       $application_email = $this->getApplicationEmail();
       $default_user_phid = $application_email->getConfigValue(
         PhabricatorMetaMTAApplicationEmail::CONFIG_DEFAULT_AUTHOR);
 
       if ($default_user_phid) {
         $user = id(new PhabricatorUser())->loadOneWhere(
           'phid = %s',
           $default_user_phid);
         if ($user) {
           return $user;
         }
       }
 
       $reasons[] = pht(
         "Phabricator is misconfigured, the application email ".
         "'%s' is set to user '%s' but that user does not exist.",
         $application_email->getAddress(),
         $default_user_phid);
     }
 
     $reasons = implode("\n\n", $reasons);
 
     throw new PhabricatorMetaMTAReceivedMailProcessingException(
       MetaMTAReceivedMailStatus::STATUS_UNKNOWN_SENDER,
       $reasons);
   }
 
   /**
    * Determine if two inbound email addresses are effectively identical. This
    * method strips and normalizes addresses so that equivalent variations are
    * correctly detected as identical. For example, these addresses are all
    * considered to match one another:
    *
    *   "Abraham Lincoln" <alincoln@example.com>
    *   alincoln@example.com
    *   <ALincoln@example.com>
    *   "Abraham" <phabricator+ALINCOLN@EXAMPLE.COM> # With configured prefix.
    *
    * @param   string  Email address.
    * @param   string  Another email address.
    * @return  bool    True if addresses match.
    */
   public static function matchAddresses($u, $v) {
     $u = self::getRawAddress($u);
     $v = self::getRawAddress($v);
 
     $u = self::stripMailboxPrefix($u);
     $v = self::stripMailboxPrefix($v);
 
     return ($u === $v);
   }
 
 
   /**
    * Strip a global mailbox prefix from an address if it is present. Phabricator
    * can be configured to prepend a prefix to all reply addresses, which can
    * make forwarding rules easier to write. A prefix looks like:
    *
    *  example@phabricator.example.com              # No Prefix
    *  phabricator+example@phabricator.example.com  # Prefix "phabricator"
    *
    * @param   string  Email address, possibly with a mailbox prefix.
    * @return  string  Email address with any prefix stripped.
    */
   public static function stripMailboxPrefix($address) {
     $address = id(new PhutilEmailAddress($address))->getAddress();
 
     $prefix_key = 'metamta.single-reply-handler-prefix';
     $prefix = PhabricatorEnv::getEnvConfig($prefix_key);
 
     $len = strlen($prefix);
 
     if ($len) {
       $prefix = $prefix.'+';
       $len = $len + 1;
     }
 
     if ($len) {
       if (!strncasecmp($address, $prefix, $len)) {
         $address = substr($address, strlen($prefix));
       }
     }
 
     return $address;
   }
 
 
   /**
    * Reduce an email address to its canonical form. For example, an adddress
    * like:
    *
    *  "Abraham Lincoln" < ALincoln@example.com >
    *
    * ...will be reduced to:
    *
    *  alincoln@example.com
    *
    * @param   string  Email address in noncanonical form.
    * @return  string  Canonical email address.
    */
   public static function getRawAddress($address) {
     $address = id(new PhutilEmailAddress($address))->getAddress();
     return trim(phutil_utf8_strtolower($address));
   }
 
 }
diff --git a/src/applications/metamta/replyhandler/PhabricatorMailReplyHandler.php b/src/applications/metamta/replyhandler/PhabricatorMailReplyHandler.php
index 98d6a46fc..efae153a0 100644
--- a/src/applications/metamta/replyhandler/PhabricatorMailReplyHandler.php
+++ b/src/applications/metamta/replyhandler/PhabricatorMailReplyHandler.php
@@ -1,394 +1,394 @@
 <?php
 
-abstract class PhabricatorMailReplyHandler {
+abstract class PhabricatorMailReplyHandler extends Phobject {
 
   private $mailReceiver;
   private $applicationEmail;
   private $actor;
   private $excludePHIDs = array();
 
   final public function setMailReceiver($mail_receiver) {
     $this->validateMailReceiver($mail_receiver);
     $this->mailReceiver = $mail_receiver;
     return $this;
   }
 
   final public function getMailReceiver() {
     return $this->mailReceiver;
   }
 
   public function setApplicationEmail(
     PhabricatorMetaMTAApplicationEmail $email) {
     $this->applicationEmail = $email;
     return $this;
   }
 
   public function getApplicationEmail() {
     return $this->applicationEmail;
   }
 
   final public function setActor(PhabricatorUser $actor) {
     $this->actor = $actor;
     return $this;
   }
 
   final public function getActor() {
     return $this->actor;
   }
 
   final public function setExcludeMailRecipientPHIDs(array $exclude) {
     $this->excludePHIDs = $exclude;
     return $this;
   }
 
   final public function getExcludeMailRecipientPHIDs() {
     return $this->excludePHIDs;
   }
 
   abstract public function validateMailReceiver($mail_receiver);
   abstract public function getPrivateReplyHandlerEmailAddress(
     PhabricatorUser $user);
 
   public function getReplyHandlerDomain() {
     return PhabricatorEnv::getEnvConfig('metamta.reply-handler-domain');
   }
 
   abstract protected function receiveEmail(
     PhabricatorMetaMTAReceivedMail $mail);
 
   public function processEmail(PhabricatorMetaMTAReceivedMail $mail) {
     $this->dropEmptyMail($mail);
 
     return $this->receiveEmail($mail);
   }
 
   private function dropEmptyMail(PhabricatorMetaMTAReceivedMail $mail) {
     $body = $mail->getCleanTextBody();
     $attachments = $mail->getAttachments();
 
     if (strlen($body) || $attachments) {
       return;
     }
 
      // Only send an error email if the user is talking to just Phabricator.
      // We can assume if there is only one "To" address it is a Phabricator
      // address since this code is running and everything.
     $is_direct_mail = (count($mail->getToAddresses()) == 1) &&
                       (count($mail->getCCAddresses()) == 0);
 
     if ($is_direct_mail) {
       $status_code = MetaMTAReceivedMailStatus::STATUS_EMPTY;
     } else {
       $status_code = MetaMTAReceivedMailStatus::STATUS_EMPTY_IGNORED;
     }
 
     throw new PhabricatorMetaMTAReceivedMailProcessingException(
       $status_code,
       pht(
         'Your message does not contain any body text or attachments, so '.
         'Phabricator can not do anything useful with it. Make sure comment '.
         'text appears at the top of your message: quoted replies, inline '.
         'text, and signatures are discarded and ignored.'));
   }
 
   public function supportsPrivateReplies() {
     return (bool)$this->getReplyHandlerDomain() &&
            !$this->supportsPublicReplies();
   }
 
   public function supportsPublicReplies() {
     if (!PhabricatorEnv::getEnvConfig('metamta.public-replies')) {
       return false;
     }
 
     if (!$this->getReplyHandlerDomain()) {
       return false;
     }
 
     return (bool)$this->getPublicReplyHandlerEmailAddress();
   }
 
   final public function supportsReplies() {
     return $this->supportsPrivateReplies() ||
            $this->supportsPublicReplies();
   }
 
   public function getPublicReplyHandlerEmailAddress() {
     return null;
   }
 
   protected function getDefaultPublicReplyHandlerEmailAddress($prefix) {
 
     $receiver = $this->getMailReceiver();
     $receiver_id = $receiver->getID();
     $domain = $this->getReplyHandlerDomain();
 
     // We compute a hash using the object's own PHID to prevent an attacker
     // from blindly interacting with objects that they haven't ever received
     // mail about by just sending to D1@, D2@, etc...
     $hash = PhabricatorObjectMailReceiver::computeMailHash(
       $receiver->getMailKey(),
       $receiver->getPHID());
 
     $address = "{$prefix}{$receiver_id}+public+{$hash}@{$domain}";
     return $this->getSingleReplyHandlerPrefix($address);
   }
 
   protected function getSingleReplyHandlerPrefix($address) {
     $single_handle_prefix = PhabricatorEnv::getEnvConfig(
       'metamta.single-reply-handler-prefix');
     return ($single_handle_prefix)
       ? $single_handle_prefix.'+'.$address
       : $address;
   }
 
   protected function getDefaultPrivateReplyHandlerEmailAddress(
     PhabricatorUser $user,
     $prefix) {
 
     $receiver = $this->getMailReceiver();
     $receiver_id = $receiver->getID();
     $user_id = $user->getID();
     $hash = PhabricatorObjectMailReceiver::computeMailHash(
       $receiver->getMailKey(),
       $user->getPHID());
     $domain = $this->getReplyHandlerDomain();
 
     $address = "{$prefix}{$receiver_id}+{$user_id}+{$hash}@{$domain}";
     return $this->getSingleReplyHandlerPrefix($address);
   }
 
   final protected function enhanceBodyWithAttachments(
     $body,
     array $attachments) {
 
     if (!$attachments) {
       return $body;
     }
 
     $files = id(new PhabricatorFileQuery())
       ->setViewer($this->getActor())
       ->withPHIDs($attachments)
       ->execute();
 
     $output = array();
     $output[] = $body;
 
     // We're going to put all the non-images first in a list, then embed
     // the images.
     $head = array();
     $tail = array();
     foreach ($files as $file) {
       if ($file->isViewableImage()) {
         $tail[] = $file;
       } else {
         $head[] = $file;
       }
     }
 
     if ($head) {
       $list = array();
       foreach ($head as $file) {
         $list[] = '  - {'.$file->getMonogram().', layout=link}';
       }
       $output[] = implode("\n", $list);
     }
 
     if ($tail) {
       $list = array();
       foreach ($tail as $file) {
         $list[] = '{'.$file->getMonogram().'}';
       }
       $output[] = implode("\n\n", $list);
     }
 
     $output = implode("\n\n", $output);
 
     return rtrim($output);
   }
 
 
   /**
    * Produce a list of mail targets for a given to/cc list.
    *
    * Each target should be sent a separate email, and contains the information
    * required to generate it with appropriate permissions and configuration.
    *
    * @param list<phid> List of "To" PHIDs.
    * @param list<phid> List of "CC" PHIDs.
    * @return list<PhabricatorMailTarget> List of targets.
    */
   final public function getMailTargets(array $raw_to, array $raw_cc) {
     list($to, $cc) = $this->expandRecipientPHIDs($raw_to, $raw_cc);
     list($to, $cc) = $this->loadRecipientUsers($to, $cc);
     list($to, $cc) = $this->filterRecipientUsers($to, $cc);
 
     if (!$to && !$cc) {
       return array();
     }
 
     $template = id(new PhabricatorMailTarget())
       ->setRawToPHIDs($raw_to)
       ->setRawCCPHIDs($raw_cc);
 
     // Set the public reply address as the default, if one exists. We
     // might replace this with a private address later.
     if ($this->supportsPublicReplies()) {
       $reply_to = $this->getPublicReplyHandlerEmailAddress();
       if ($reply_to) {
         $template->setReplyTo($reply_to);
       }
     }
 
     $supports_private_replies = $this->supportsPrivateReplies();
     $mail_all = !PhabricatorEnv::getEnvConfig('metamta.one-mail-per-recipient');
     $targets = array();
     if ($mail_all) {
       $target = id(clone $template)
         ->setViewer(PhabricatorUser::getOmnipotentUser())
         ->setToMap($to)
         ->setCCMap($cc);
 
       $targets[] = $target;
     } else {
       $map = $to + $cc;
 
       foreach ($map as $phid => $user) {
         $target = id(clone $template)
           ->setViewer($user)
           ->setToMap(array($phid => $user))
           ->setCCMap(array());
 
         if ($supports_private_replies) {
           $reply_to = $this->getPrivateReplyHandlerEmailAddress($user);
           if ($reply_to) {
             $target->setReplyTo($reply_to);
           }
         }
 
         $targets[] = $target;
       }
     }
 
     return $targets;
   }
 
 
   /**
    * Expand lists of recipient PHIDs.
    *
    * This takes any compound recipients (like projects) and looks up all their
    * members.
    *
    * @param list<phid> List of To PHIDs.
    * @param list<phid> List of CC PHIDs.
    * @return pair<list<phid>, list<phid>> Expanded PHID lists.
    */
   private function expandRecipientPHIDs(array $to, array $cc) {
     $to_result = array();
     $cc_result = array();
 
     $all_phids = array_merge($to, $cc);
     if ($all_phids) {
       $map = id(new PhabricatorMetaMTAMemberQuery())
         ->setViewer(PhabricatorUser::getOmnipotentUser())
         ->withPHIDs($all_phids)
         ->execute();
       foreach ($to as $phid) {
         foreach ($map[$phid] as $expanded) {
           $to_result[$expanded] = $expanded;
         }
       }
       foreach ($cc as $phid) {
         foreach ($map[$phid] as $expanded) {
           $cc_result[$expanded] = $expanded;
         }
       }
     }
 
     // Remove recipients from "CC" if they're also present in "To".
     $cc_result = array_diff_key($cc_result, $to_result);
 
     return array(array_values($to_result), array_values($cc_result));
   }
 
 
   /**
    * Load @{class:PhabricatorUser} objects for each recipient.
    *
    * Invalid recipients are dropped from the results.
    *
    * @param list<phid> List of To PHIDs.
    * @param list<phid> List of CC PHIDs.
    * @return pair<wild, wild> Maps from PHIDs to users.
    */
   private function loadRecipientUsers(array $to, array $cc) {
     $to_result = array();
     $cc_result = array();
 
     $all_phids = array_merge($to, $cc);
     if ($all_phids) {
       $users = id(new PhabricatorPeopleQuery())
         ->setViewer(PhabricatorUser::getOmnipotentUser())
         ->withPHIDs($all_phids)
         ->execute();
       $users = mpull($users, null, 'getPHID');
 
       foreach ($to as $phid) {
         if (isset($users[$phid])) {
           $to_result[$phid] = $users[$phid];
         }
       }
       foreach ($cc as $phid) {
         if (isset($users[$phid])) {
           $cc_result[$phid] = $users[$phid];
         }
       }
     }
 
     return array($to_result, $cc_result);
   }
 
 
   /**
    * Remove recipients who do not have permission to view the mail receiver.
    *
    * @param map<string, PhabricatorUser> Map of "To" users.
    * @param map<string, PhabricatorUser> Map of "CC" users.
    * @return pair<wild, wild> Filtered user maps.
    */
   private function filterRecipientUsers(array $to, array $cc) {
     $to_result = array();
     $cc_result = array();
 
     $all_users = $to + $cc;
     if ($all_users) {
       $can_see = array();
       $object = $this->getMailReceiver();
       foreach ($all_users as $phid => $user) {
         $visible = PhabricatorPolicyFilter::hasCapability(
           $user,
           $object,
           PhabricatorPolicyCapability::CAN_VIEW);
         if ($visible) {
           $can_see[$phid] = true;
         }
       }
 
       foreach ($to as $phid => $user) {
         if (!empty($can_see[$phid])) {
           $to_result[$phid] = $all_users[$phid];
         }
       }
 
       foreach ($cc as $phid => $user) {
         if (!empty($can_see[$phid])) {
           $cc_result[$phid] = $all_users[$phid];
         }
       }
     }
 
     return array($to_result, $cc_result);
   }
 
 }
diff --git a/src/applications/metamta/storage/PhabricatorMetaMTAApplicationEmail.php b/src/applications/metamta/storage/PhabricatorMetaMTAApplicationEmail.php
index 561301f7a..fb70b377a 100644
--- a/src/applications/metamta/storage/PhabricatorMetaMTAApplicationEmail.php
+++ b/src/applications/metamta/storage/PhabricatorMetaMTAApplicationEmail.php
@@ -1,112 +1,157 @@
 <?php
 
 final class PhabricatorMetaMTAApplicationEmail
   extends PhabricatorMetaMTADAO
-  implements PhabricatorPolicyInterface {
+  implements
+    PhabricatorPolicyInterface,
+    PhabricatorApplicationTransactionInterface,
+    PhabricatorDestructibleInterface,
+    PhabricatorSpacesInterface {
 
   protected $applicationPHID;
   protected $address;
   protected $configData;
+  protected $spacePHID;
 
   private $application = self::ATTACHABLE;
 
   const CONFIG_DEFAULT_AUTHOR = 'config:default:author';
 
   protected function getConfiguration() {
     return array(
       self::CONFIG_AUX_PHID => true,
       self::CONFIG_SERIALIZATION => array(
         'configData' => self::SERIALIZATION_JSON,
       ),
       self::CONFIG_COLUMN_SCHEMA => array(
         'address' => 'sort128',
       ),
       self::CONFIG_KEY_SCHEMA => array(
         'key_address' => array(
           'columns' => array('address'),
           'unique' => true,
         ),
         'key_application' => array(
           'columns' => array('applicationPHID'),
         ),
       ),
     ) + parent::getConfiguration();
   }
 
   public function generatePHID() {
     return PhabricatorPHID::generateNewPHID(
       PhabricatorMetaMTAApplicationEmailPHIDType::TYPECONST);
   }
 
   public static function initializeNewAppEmail(PhabricatorUser $actor) {
     return id(new PhabricatorMetaMTAApplicationEmail())
+      ->setSpacePHID($actor->getDefaultSpacePHID())
       ->setConfigData(array());
   }
 
   public function attachApplication(PhabricatorApplication $app) {
     $this->application = $app;
     return $this;
   }
 
   public function getApplication() {
     return self::assertAttached($this->application);
   }
 
   public function setConfigValue($key, $value) {
     $this->configData[$key] = $value;
     return $this;
   }
 
   public function getConfigValue($key, $default = null) {
     return idx($this->configData, $key, $default);
   }
 
 
   public function getInUseMessage() {
     $applications = PhabricatorApplication::getAllApplications();
     $applications = mpull($applications, null, 'getPHID');
     $application = idx(
       $applications,
       $this->getApplicationPHID());
     if ($application) {
       $message = pht(
         'The address %s is configured to be used by the %s Application.',
         $this->getAddress(),
         $application->getName());
     } else {
       $message = pht(
         'The address %s is configured to be used by an application.',
         $this->getAddress());
     }
 
     return $message;
   }
 
 /* -(  PhabricatorPolicyInterface  )----------------------------------------- */
 
 
   public function getCapabilities() {
     return array(
       PhabricatorPolicyCapability::CAN_VIEW,
       PhabricatorPolicyCapability::CAN_EDIT,
     );
   }
 
   public function getPolicy($capability) {
     return $this->getApplication()->getPolicy($capability);
   }
 
   public function hasAutomaticCapability(
     $capability,
     PhabricatorUser $viewer) {
 
     return $this->getApplication()->hasAutomaticCapability(
       $capability,
       $viewer);
   }
 
   public function describeAutomaticCapability($capability) {
     return $this->getApplication()->describeAutomaticCapability($capability);
   }
 
+
+/* -(  PhabricatorApplicationTransactionInterface  )------------------------- */
+
+
+  public function getApplicationTransactionEditor() {
+    return new PhabricatorMetaMTAApplicationEmailEditor();
+  }
+
+  public function getApplicationTransactionObject() {
+    return $this;
+  }
+
+  public function getApplicationTransactionTemplate() {
+    return new PhabricatorMetaMTAApplicationEmailTransaction();
+  }
+
+  public function willRenderTimeline(
+    PhabricatorApplicationTransactionView $timeline,
+    AphrontRequest $request) {
+    return $timeline;
+  }
+
+
+/* -(  PhabricatorDestructibleInterface  )----------------------------------- */
+
+
+  public function destroyObjectPermanently(
+    PhabricatorDestructionEngine $engine) {
+    $this->delete();
+  }
+
+
+/* -(  PhabricatorSpacesInterface  )----------------------------------------- */
+
+
+  public function getSpacePHID() {
+    return $this->spacePHID;
+  }
+
 }
diff --git a/src/applications/metamta/storage/PhabricatorMetaMTAApplicationEmailTransaction.php b/src/applications/metamta/storage/PhabricatorMetaMTAApplicationEmailTransaction.php
new file mode 100644
index 000000000..019adb338
--- /dev/null
+++ b/src/applications/metamta/storage/PhabricatorMetaMTAApplicationEmailTransaction.php
@@ -0,0 +1,23 @@
+<?php
+
+final class PhabricatorMetaMTAApplicationEmailTransaction
+  extends PhabricatorApplicationTransaction {
+
+  const KEY_CONFIG = 'appemail.config.key';
+
+  const TYPE_ADDRESS = 'appemail.address';
+  const TYPE_CONFIG = 'appemail.config';
+
+  public function getApplicationName() {
+    return 'metamta';
+  }
+
+  public function getApplicationTransactionType() {
+    return PhabricatorMetaMTAApplicationEmailPHIDType::TYPECONST;
+  }
+
+  public function getApplicationTransactionCommentObject() {
+    return null;
+  }
+
+}
diff --git a/src/applications/metamta/storage/PhabricatorMetaMTAAttachment.php b/src/applications/metamta/storage/PhabricatorMetaMTAAttachment.php
index d846862e1..96d2f50f0 100644
--- a/src/applications/metamta/storage/PhabricatorMetaMTAAttachment.php
+++ b/src/applications/metamta/storage/PhabricatorMetaMTAAttachment.php
@@ -1,56 +1,56 @@
 <?php
 
-final class PhabricatorMetaMTAAttachment {
+final class PhabricatorMetaMTAAttachment extends Phobject {
   protected $data;
   protected $filename;
   protected $mimetype;
 
   public function __construct($data, $filename, $mimetype) {
     $this->setData($data);
     $this->setFilename($filename);
     $this->setMimeType($mimetype);
   }
 
   public function getData() {
     return $this->data;
   }
 
   public function setData($data) {
     $this->data = $data;
     return $this;
   }
 
   public function getFilename() {
     return $this->filename;
   }
 
   public function setFilename($filename) {
     $this->filename = $filename;
     return $this;
   }
 
   public function getMimeType() {
     return $this->mimetype;
   }
 
   public function setMimeType($mimetype) {
     $this->mimetype = $mimetype;
     return $this;
   }
 
   public function toDictionary() {
     return array(
       'filename' => $this->getFilename(),
       'mimetype' => $this->getMimetype(),
       'data' => $this->getData(),
     );
   }
 
   public static function newFromDictionary(array $dict) {
     return new PhabricatorMetaMTAAttachment(
       idx($dict, 'data'),
       idx($dict, 'filename'),
       idx($dict, 'mimetype'));
   }
 
 }
diff --git a/src/applications/metamta/view/PhabricatorMetaMTAMailBody.php b/src/applications/metamta/view/PhabricatorMetaMTAMailBody.php
index d8061c27a..3e0a6d9b1 100644
--- a/src/applications/metamta/view/PhabricatorMetaMTAMailBody.php
+++ b/src/applications/metamta/view/PhabricatorMetaMTAMailBody.php
@@ -1,215 +1,215 @@
 <?php
 
 /**
  * Render the body of an application email by building it up section-by-section.
  *
  * @task compose  Composition
  * @task render   Rendering
  */
-final class PhabricatorMetaMTAMailBody {
+final class PhabricatorMetaMTAMailBody extends Phobject {
 
   private $sections = array();
   private $htmlSections = array();
   private $attachments = array();
 
   private $viewer;
 
   public function getViewer() {
     return $this->viewer;
   }
 
   public function setViewer($viewer) {
     $this->viewer = $viewer;
   }
 
 /* -(  Composition  )-------------------------------------------------------- */
 
 
   /**
    * Add a raw block of text to the email. This will be rendered as-is.
    *
    * @param string Block of text.
    * @return this
    * @task compose
    */
   public function addRawSection($text) {
     if (strlen($text)) {
       $text = rtrim($text);
       $this->sections[] = $text;
       $this->htmlSections[] = phutil_escape_html_newlines(
         phutil_tag('div', array(), $text));
     }
     return $this;
   }
 
   public function addRemarkupSection($text) {
     try {
       $engine = PhabricatorMarkupEngine::newMarkupEngine(array());
       $engine->setConfig('viewer', $this->getViewer());
       $engine->setMode(PhutilRemarkupEngine::MODE_TEXT);
       $styled_text = $engine->markupText($text);
       $this->sections[] = $styled_text;
     } catch (Exception $ex) {
       phlog($ex);
       $this->sections[] = $text;
     }
 
     try {
       $mail_engine = PhabricatorMarkupEngine::newMarkupEngine(array());
       $mail_engine->setConfig('viewer', $this->getViewer());
       $mail_engine->setMode(PhutilRemarkupEngine::MODE_HTML_MAIL);
       $mail_engine->setConfig(
         'uri.base',
         PhabricatorEnv::getProductionURI('/'));
       $html = $mail_engine->markupText($text);
       $this->htmlSections[] = $html;
     } catch (Exception $ex) {
       phlog($ex);
       $this->htmlSections[] = phutil_escape_html_newlines(
         phutil_tag(
           'div',
           array(),
           $text));
     }
 
     return $this;
   }
 
   public function addRawPlaintextSection($text) {
     if (strlen($text)) {
       $text = rtrim($text);
       $this->sections[] = $text;
     }
     return $this;
   }
 
   public function addRawHTMLSection($html) {
     $this->htmlSections[] = phutil_safe_html($html);
     return $this;
   }
 
 
   /**
    * Add a block of text with a section header. This is rendered like this:
    *
    *    HEADER
    *      Text is indented.
    *
    * @param string Header text.
    * @param string Section text.
    * @return this
    * @task compose
    */
   public function addTextSection($header, $section) {
     if ($section instanceof PhabricatorMetaMTAMailSection) {
       $plaintext = $section->getPlaintext();
       $html = $section->getHTML();
     } else {
       $plaintext = $section;
       $html = phutil_escape_html_newlines(phutil_tag('div', array(), $section));
     }
 
     $this->addPlaintextSection($header, $plaintext);
     $this->addHTMLSection($header, $html);
     return $this;
   }
 
   public function addPlaintextSection($header, $text) {
     $this->sections[] = $header."\n".$this->indent($text);
     return $this;
   }
 
   public function addHTMLSection($header, $html_fragment) {
     $this->htmlSections[] = array(
       phutil_tag(
         'div',
         array(),
         array(
           phutil_tag('strong', array(), $header),
           phutil_tag('div', array(), $html_fragment),
         )),
     );
     return $this;
   }
 
   public function addLinkSection($header, $link) {
     $html = phutil_tag('a', array('href' => $link), $link);
     $this->addPlaintextSection($header, $link);
     $this->addHTMLSection($header, $html);
     return $this;
   }
 
   /**
    * Add a Herald section with a rule management URI and a transcript URI.
    *
    * @param string URI to rule transcripts.
    * @return this
    * @task compose
    */
   public function addHeraldSection($xscript_uri) {
     if (!PhabricatorEnv::getEnvConfig('metamta.herald.show-hints')) {
       return $this;
     }
 
     $this->addLinkSection(
       pht('WHY DID I GET THIS EMAIL?'),
       PhabricatorEnv::getProductionURI($xscript_uri));
 
     return $this;
   }
 
   /**
    * Add an attachment.
    *
    * @param PhabricatorMetaMTAAttachment Attachment.
    * @return this
    * @task compose
    */
   public function addAttachment(PhabricatorMetaMTAAttachment $attachment) {
     $this->attachments[] = $attachment;
     return $this;
   }
 
 
 /* -(  Rendering  )---------------------------------------------------------- */
 
 
   /**
    * Render the email body.
    *
    * @return string Rendered body.
    * @task render
    */
   public function render() {
     return implode("\n\n", $this->sections)."\n";
   }
 
   public function renderHTML() {
     $br = phutil_tag('br');
     $body = phutil_implode_html($br, $this->htmlSections);
     return (string)hsprintf('%s', array($body, $br));
   }
 
   /**
    * Retrieve attachments.
    *
    * @return list<PhabricatorMetaMTAAttachment> Attachments.
    * @task render
    */
   public function getAttachments() {
     return $this->attachments;
   }
 
 
   /**
    * Indent a block of text for rendering under a section heading.
    *
    * @param string Text to indent.
    * @return string Indented text.
    * @task render
    */
   private function indent($text) {
     return rtrim("  ".str_replace("\n", "\n  ", $text));
   }
 
 }
diff --git a/src/applications/metamta/view/PhabricatorMetaMTAMailSection.php b/src/applications/metamta/view/PhabricatorMetaMTAMailSection.php
index 701511dda..536ab923b 100644
--- a/src/applications/metamta/view/PhabricatorMetaMTAMailSection.php
+++ b/src/applications/metamta/view/PhabricatorMetaMTAMailSection.php
@@ -1,39 +1,39 @@
 <?php
+
 /**
  * Helper for building a rendered section.
  *
  * @task compose  Composition
  * @task render   Rendering
  * @group metamta
  */
-
-final class PhabricatorMetaMTAMailSection {
+final class PhabricatorMetaMTAMailSection extends Phobject {
   private $plaintextFragments = array();
   private $htmlFragments = array();
 
   public function getHTML() {
     return $this->htmlFragments;
   }
 
   public function getPlaintext() {
     return implode("\n", $this->plaintextFragments);
   }
 
   public function addHTMLFragment($fragment) {
     $this->htmlFragments[] = $fragment;
     return $this;
   }
 
   public function addPlaintextFragment($fragment) {
     $this->plaintextFragments[] = $fragment;
     return $this;
   }
 
   public function addFragment($fragment) {
     $this->plaintextFragments[] = $fragment;
     $this->htmlFragments[] =
       phutil_escape_html_newlines(phutil_tag('div', array(), $fragment));
 
     return $this;
   }
 }
diff --git a/src/applications/multimeter/data/MultimeterControl.php b/src/applications/multimeter/data/MultimeterControl.php
index 892e9e56c..1b01ce2e3 100644
--- a/src/applications/multimeter/data/MultimeterControl.php
+++ b/src/applications/multimeter/data/MultimeterControl.php
@@ -1,292 +1,292 @@
 <?php
 
-final class MultimeterControl {
+final class MultimeterControl extends Phobject {
 
   private static $instance;
 
   private $events = array();
   private $sampleRate;
   private $pauseDepth;
 
   private $eventViewer;
   private $eventContext;
 
   private function __construct() {
     // Private.
   }
 
   public static function newInstance() {
     $instance = new MultimeterControl();
 
     // NOTE: We don't set the sample rate yet. This allows the multimeter to
     // be initialized and begin recording events, then make a decision about
     // whether the page will be sampled or not later on (once we've loaded
     // enough configuration).
 
     self::$instance = $instance;
     return self::getInstance();
   }
 
   public static function getInstance() {
     return self::$instance;
   }
 
   public function isActive() {
     return ($this->sampleRate !== 0) && ($this->pauseDepth == 0);
   }
 
   public function setSampleRate($rate) {
     if ($rate && (mt_rand(1, $rate) == $rate)) {
       $sample_rate = $rate;
     } else {
       $sample_rate = 0;
     }
 
     $this->sampleRate = $sample_rate;
 
     return;
   }
 
   public function pauseMultimeter() {
     $this->pauseDepth++;
     return $this;
   }
 
   public function unpauseMultimeter() {
     if (!$this->pauseDepth) {
       throw new Exception(pht('Trying to unpause an active multimeter!'));
     }
     $this->pauseDepth--;
     return $this;
   }
 
 
   public function newEvent($type, $label, $cost) {
     if (!$this->isActive()) {
       return null;
     }
 
     $event = id(new MultimeterEvent())
       ->setEventType($type)
       ->setEventLabel($label)
       ->setResourceCost($cost)
       ->setEpoch(PhabricatorTime::getNow());
 
     $this->events[] = $event;
 
     return $event;
   }
 
   public function saveEvents() {
     if (!$this->isActive()) {
       return;
     }
 
     $events = $this->events;
     if (!$events) {
       return;
     }
 
     if ($this->sampleRate === null) {
       throw new PhutilInvalidStateException('setSampleRate');
     }
 
     $this->addServiceEvents();
 
     // Don't sample any of this stuff.
     $this->pauseMultimeter();
 
     $use_scope = AphrontWriteGuard::isGuardActive();
     if ($use_scope) {
       $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
     } else {
       AphrontWriteGuard::allowDangerousUnguardedWrites(true);
     }
 
     $caught = null;
     try {
       $this->writeEvents();
     } catch (Exception $ex) {
       $caught = $ex;
     }
 
     if ($use_scope) {
       unset($unguarded);
     } else {
       AphrontWriteGuard::allowDangerousUnguardedWrites(false);
     }
 
     $this->unpauseMultimeter();
 
     if ($caught) {
       throw $caught;
     }
   }
 
   private function writeEvents() {
     $events = $this->events;
 
     $random = Filesystem::readRandomBytes(32);
     $request_key = PhabricatorHash::digestForIndex($random);
 
     $host_id = $this->loadHostID(php_uname('n'));
     $context_id = $this->loadEventContextID($this->eventContext);
     $viewer_id = $this->loadEventViewerID($this->eventViewer);
     $label_map = $this->loadEventLabelIDs(mpull($events, 'getEventLabel'));
 
     foreach ($events as $event) {
       $event
         ->setRequestKey($request_key)
         ->setSampleRate($this->sampleRate)
         ->setEventHostID($host_id)
         ->setEventContextID($context_id)
         ->setEventViewerID($viewer_id)
         ->setEventLabelID($label_map[$event->getEventLabel()])
         ->save();
     }
   }
 
   public function setEventContext($event_context) {
     $this->eventContext = $event_context;
     return $this;
   }
 
   public function getEventContext() {
     return $this->eventContext;
   }
 
   public function setEventViewer($viewer) {
     $this->eventViewer = $viewer;
     return $this;
   }
 
   private function loadHostID($host) {
     $map = $this->loadDimensionMap(new MultimeterHost(), array($host));
     return idx($map, $host);
   }
 
   private function loadEventViewerID($viewer) {
     $map = $this->loadDimensionMap(new MultimeterViewer(), array($viewer));
     return idx($map, $viewer);
   }
 
   private function loadEventContextID($context) {
     $map = $this->loadDimensionMap(new MultimeterContext(), array($context));
     return idx($map, $context);
   }
 
   private function loadEventLabelIDs(array $labels) {
     return $this->loadDimensionMap(new MultimeterLabel(), $labels);
   }
 
   private function loadDimensionMap(MultimeterDimension $table, array $names) {
     $hashes = array();
     foreach ($names as $name) {
       $hashes[] = PhabricatorHash::digestForIndex($name);
     }
 
     $objects = $table->loadAllWhere('nameHash IN (%Ls)', $hashes);
     $map = mpull($objects, 'getID', 'getName');
 
     $need = array();
     foreach ($names as $name) {
       if (isset($map[$name])) {
         continue;
       }
       $need[$name] = $name;
     }
 
     foreach ($need as $name) {
       $object = id(clone $table)
         ->setName($name)
         ->save();
       $map[$name] = $object->getID();
     }
 
     return $map;
   }
 
   private function addServiceEvents() {
     $events = PhutilServiceProfiler::getInstance()->getServiceCallLog();
     foreach ($events as $event) {
       $type = idx($event, 'type');
       switch ($type) {
         case 'exec':
           $this->newEvent(
             MultimeterEvent::TYPE_EXEC_TIME,
             $label = $this->getLabelForCommandEvent($event['command']),
             (1000000 * $event['duration']));
           break;
       }
     }
   }
 
   private function getLabelForCommandEvent($command) {
     $argv = preg_split('/\s+/', $command);
 
     $bin = array_shift($argv);
     $bin = basename($bin);
     $bin = trim($bin, '"\'');
 
     // It's important to avoid leaking details about command parameters,
     // because some may be sensitive. Given this, it's not trivial to
     // determine which parts of a command are arguments and which parts are
     // flags.
 
     // Rather than try too hard for now, just whitelist some workflows that we
     // know about and record everything else generically. Overall, this will
     // produce labels like "pygmentize" or "git log", discarding all flags and
     // arguments.
 
     $workflows = array(
       'git' => array(
         'log' => true,
         'for-each-ref' => true,
         'pull' => true,
         'clone' => true,
         'fetch' => true,
         'cat-file' => true,
         'init' => true,
         'config' => true,
         'remote' => true,
         'rev-parse' => true,
         'diff' => true,
         'ls-tree' => true,
       ),
       'svn' => array(
         'log' => true,
         'diff' => true,
       ),
       'hg' => array(
         'log' => true,
         'locate' => true,
         'pull' => true,
         'clone' => true,
         'init' => true,
         'diff' => true,
         'cat' => true,
       ),
       'svnadmin' => array(
         'create' => true,
       ),
     );
 
     $workflow = null;
     $candidates = idx($workflows, $bin);
     if ($candidates) {
       foreach ($argv as $arg) {
         if (isset($candidates[$arg])) {
           $workflow = $arg;
           break;
         }
       }
     }
 
     if ($workflow) {
       return 'bin.'.$bin.' '.$workflow;
     } else {
       return 'bin.'.$bin;
     }
   }
 
 }
diff --git a/src/applications/notification/builder/PhabricatorNotificationBuilder.php b/src/applications/notification/builder/PhabricatorNotificationBuilder.php
index b39baf456..57cf56877 100644
--- a/src/applications/notification/builder/PhabricatorNotificationBuilder.php
+++ b/src/applications/notification/builder/PhabricatorNotificationBuilder.php
@@ -1,117 +1,117 @@
 <?php
 
-final class PhabricatorNotificationBuilder {
+final class PhabricatorNotificationBuilder extends Phobject {
 
   private $stories;
   private $user = null;
 
   public function __construct(array $stories) {
     $this->stories = $stories;
   }
 
   public function setUser($user) {
     $this->user = $user;
     return $this;
   }
 
   public function buildView() {
 
     $stories = $this->stories;
     $stories = mpull($stories, null, 'getChronologicalKey');
 
     // Aggregate notifications. Generally, we can aggregate notifications only
     // by object, e.g. "a updated T123" and "b updated T123" can become
     // "a and b updated T123", but we can't combine "a updated T123" and
     // "a updated T234" into "a updated T123 and T234" because there would be
     // nowhere sensible for the notification to link to, and no reasonable way
     // to unambiguously clear it.
 
     // Build up a map of all the possible aggregations.
 
     $chronokey_map = array();
     $aggregation_map = array();
     $agg_types = array();
     foreach ($stories as $chronokey => $story) {
       $chronokey_map[$chronokey] = $story->getNotificationAggregations();
       foreach ($chronokey_map[$chronokey] as $key => $type) {
         $agg_types[$key] = $type;
         $aggregation_map[$key]['keys'][$chronokey] = true;
       }
     }
 
     // Repeatedly select the largest available aggregation until none remain.
 
     $aggregated_stories = array();
     while ($aggregation_map) {
 
       // Count the size of each aggregation, removing any which will consume
       // fewer than 2 stories.
 
       foreach ($aggregation_map as $key => $dict) {
         $size = count($dict['keys']);
         if ($size > 1) {
           $aggregation_map[$key]['size'] = $size;
         } else {
           unset($aggregation_map[$key]);
         }
       }
 
       // If we're out of aggregations, break out.
 
       if (!$aggregation_map) {
         break;
       }
 
       // Select the aggregation we're going to make, and remove it from the
       // map.
 
       $aggregation_map = isort($aggregation_map, 'size');
       $agg_info = idx(last($aggregation_map), 'keys');
       $agg_key  = last_key($aggregation_map);
       unset($aggregation_map[$agg_key]);
 
       // Select all the stories it aggregates, and remove them from the master
       // list of stories and from all other possible aggregations.
 
       $sub_stories = array();
       foreach ($agg_info as $chronokey => $ignored) {
         $sub_stories[$chronokey] = $stories[$chronokey];
         unset($stories[$chronokey]);
         foreach ($chronokey_map[$chronokey] as $key => $type) {
           unset($aggregation_map[$key]['keys'][$chronokey]);
         }
         unset($chronokey_map[$chronokey]);
       }
 
       // Build the aggregate story.
 
       krsort($sub_stories);
       $story_class = $agg_types[$agg_key];
       $conv = array(head($sub_stories)->getStoryData());
 
       $new_story = newv($story_class, $conv);
       $new_story->setAggregateStories($sub_stories);
       $aggregated_stories[] = $new_story;
     }
 
     // Combine the aggregate stories back into the list of stories.
 
     $stories = array_merge($stories, $aggregated_stories);
     $stories = mpull($stories, null, 'getChronologicalKey');
     krsort($stories);
 
     $null_view = new AphrontNullView();
 
     foreach ($stories as $story) {
       try {
         $view = $story->renderView();
       } catch (Exception $ex) {
         // TODO: Render a nice debuggable notice instead?
         continue;
       }
       $null_view->appendChild($view->renderNotification($this->user));
     }
 
     return $null_view;
   }
 }
diff --git a/src/applications/notification/client/PhabricatorNotificationClient.php b/src/applications/notification/client/PhabricatorNotificationClient.php
index b3367dd54..f527a64bc 100644
--- a/src/applications/notification/client/PhabricatorNotificationClient.php
+++ b/src/applications/notification/client/PhabricatorNotificationClient.php
@@ -1,63 +1,63 @@
 <?php
 
-final class PhabricatorNotificationClient {
+final class PhabricatorNotificationClient extends Phobject {
 
   const EXPECT_VERSION = 7;
 
   public static function getServerStatus() {
     $uri = PhabricatorEnv::getEnvConfig('notification.server-uri');
     $uri = id(new PhutilURI($uri))
       ->setPath('/status/')
       ->setQueryParam('instance', self::getInstance());
 
     // We always use HTTP to connect to the server itself: it's simpler and
     // there's no meaningful security benefit to securing this link today.
     // Force the protocol to HTTP in case users have set it to something else.
     $uri->setProtocol('http');
 
     list($body) = id(new HTTPSFuture($uri))
       ->setTimeout(3)
       ->resolvex();
 
     $status = phutil_json_decode($body);
     if (!is_array($status)) {
       throw new Exception(
         pht(
           'Expected JSON response from notification server, received: %s',
           $body));
     }
 
     return $status;
   }
 
   public static function tryToPostMessage(array $data) {
     if (!PhabricatorEnv::getEnvConfig('notification.enabled')) {
       return;
     }
 
     try {
       self::postMessage($data);
     } catch (Exception $ex) {
       // Just ignore any issues here.
       phlog($ex);
     }
   }
 
   private static function postMessage(array $data) {
     $server_uri = PhabricatorEnv::getEnvConfig('notification.server-uri');
     $server_uri = id(new PhutilURI($server_uri))
       ->setPath('/')
       ->setQueryParam('instance', self::getInstance());
 
     id(new HTTPSFuture($server_uri, json_encode($data)))
       ->setMethod('POST')
       ->setTimeout(1)
       ->resolvex();
   }
 
   private static function getInstance() {
     $client_uri = PhabricatorEnv::getEnvConfig('notification.client-uri');
     return id(new PhutilURI($client_uri))->getPath();
   }
 
 }
diff --git a/src/applications/notification/garbagecollector/FeedStoryNotificationGarbageCollector.php b/src/applications/notification/garbagecollector/FeedStoryNotificationGarbageCollector.php
new file mode 100644
index 000000000..5b7423720
--- /dev/null
+++ b/src/applications/notification/garbagecollector/FeedStoryNotificationGarbageCollector.php
@@ -0,0 +1,22 @@
+<?php
+
+final class FeedStoryNotificationGarbageCollector
+  extends PhabricatorGarbageCollector {
+
+  public function collectGarbage() {
+    $ttl = 90 * 24 * 60 * 60;
+
+    $table = new PhabricatorFeedStoryNotification();
+    $conn_w = $table->establishConnection('w');
+
+    queryfx(
+      $conn_w,
+      'DELETE FROM %T WHERE chronologicalKey < (%d << 32)
+        ORDER BY chronologicalKey ASC LIMIT 100',
+      $table->getTableName(),
+      time() - $ttl);
+
+    return ($conn_w->getAffectedRows() == 100);
+  }
+
+}
diff --git a/src/applications/notification/storage/PhabricatorFeedStoryNotification.php b/src/applications/notification/storage/PhabricatorFeedStoryNotification.php
index 8f89c987e..30aec1f28 100644
--- a/src/applications/notification/storage/PhabricatorFeedStoryNotification.php
+++ b/src/applications/notification/storage/PhabricatorFeedStoryNotification.php
@@ -1,72 +1,75 @@
 <?php
 
 final class PhabricatorFeedStoryNotification extends PhabricatorFeedDAO {
 
   protected $userPHID;
   protected $primaryObjectPHID;
   protected $chronologicalKey;
   protected $hasViewed;
 
   protected function getConfiguration() {
     return array(
       self::CONFIG_IDS          => self::IDS_MANUAL,
       self::CONFIG_TIMESTAMPS   => false,
       self::CONFIG_COLUMN_SCHEMA => array(
         'chronologicalKey' => 'uint64',
         'hasViewed' => 'bool',
         'id' => null,
       ),
       self::CONFIG_KEY_SCHEMA => array(
         'PRIMARY' => null,
         'userPHID' => array(
           'columns' => array('userPHID', 'chronologicalKey'),
           'unique' => true,
         ),
         'userPHID_2' => array(
           'columns' => array('userPHID', 'hasViewed', 'primaryObjectPHID'),
         ),
         'key_object' => array(
           'columns' => array('primaryObjectPHID'),
         ),
+        'key_chronological' => array(
+          'columns' => array('chronologicalKey'),
+        ),
       ),
     ) + parent::getConfiguration();
   }
 
   public static function updateObjectNotificationViews(
     PhabricatorUser $user,
     $object_phid) {
 
     $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
 
     $notification_table = new PhabricatorFeedStoryNotification();
     $conn = $notification_table->establishConnection('w');
 
     queryfx(
       $conn,
       'UPDATE %T
        SET hasViewed = 1
        WHERE userPHID = %s
          AND primaryObjectPHID = %s
          AND hasViewed = 0',
       $notification_table->getTableName(),
       $user->getPHID(),
       $object_phid);
 
     unset($unguarded);
   }
 
   public function countUnread(PhabricatorUser $user) {
     $conn = $this->establishConnection('r');
 
     $data = queryfx_one(
       $conn,
       'SELECT COUNT(*) as count
        FROM %T
        WHERE userPHID = %s AND hasViewed = 0',
       $this->getTableName(),
       $user->getPHID());
 
     return $data['count'];
   }
 
 }
diff --git a/src/applications/nuance/application/PhabricatorNuanceApplication.php b/src/applications/nuance/application/PhabricatorNuanceApplication.php
index be297f402..20c58f841 100644
--- a/src/applications/nuance/application/PhabricatorNuanceApplication.php
+++ b/src/applications/nuance/application/PhabricatorNuanceApplication.php
@@ -1,83 +1,85 @@
 <?php
 
 final class PhabricatorNuanceApplication extends PhabricatorApplication {
 
   public function getName() {
     return pht('Nuance');
   }
 
   public function getFontIcon() {
     return 'fa-fax';
   }
 
   public function getTitleGlyph() {
     return "\xE2\x98\x8E";
   }
 
   public function isPrototype() {
     return true;
   }
 
   public function isLaunchable() {
     // Try to hide this even more for now.
     return false;
   }
 
   public function canUninstall() {
     return true;
   }
 
   public function getBaseURI() {
     return '/nuance/';
   }
 
   public function getShortDescription() {
     return pht('High-Volume Task Queues');
   }
 
   public function getRoutes() {
     return array(
       '/nuance/' => array(
         'item/' => array(
           'view/(?P<id>[1-9]\d*)/' => 'NuanceItemViewController',
           'edit/(?P<id>[1-9]\d*)/' => 'NuanceItemEditController',
           'new/'                   => 'NuanceItemEditController',
         ),
         'source/' => array(
           '(?:query/(?P<queryKey>[^/]+)/)?' => 'NuanceSourceListController',
           'view/(?P<id>[1-9]\d*)/' => 'NuanceSourceViewController',
           'edit/(?P<id>[1-9]\d*)/' => 'NuanceSourceEditController',
           'new/(?P<type>[^/]+)/'   => 'NuanceSourceEditController',
           'create/' => 'NuanceSourceCreateController',
         ),
         'queue/' => array(
           '(?:query/(?P<queryKey>[^/]+)/)?' => 'NuanceQueueListController',
           'view/(?P<id>[1-9]\d*)/' => 'NuanceQueueViewController',
           'edit/(?P<id>[1-9]\d*)/' => 'NuanceQueueEditController',
           'new/'                   => 'NuanceQueueEditController',
         ),
         'requestor/' => array(
           'view/(?P<id>[1-9]\d*)/' => 'NuanceRequestorViewController',
           'edit/(?P<id>[1-9]\d*)/' => 'NuanceRequestorEditController',
           'new/'                   => 'NuanceRequestorEditController',
         ),
       ),
       '/action/' => array(
         '(?P<id>[1-9]\d*)/(?P<path>.*)' => 'NuanceSourceActionController',
       ),
     );
   }
 
   protected function getCustomCapabilities() {
     return array(
       NuanceSourceDefaultViewCapability::CAPABILITY => array(
         'caption' => pht('Default view policy for newly created sources.'),
+        'template' => NuanceSourcePHIDType::TYPECONST,
       ),
       NuanceSourceDefaultEditCapability::CAPABILITY => array(
         'caption' => pht('Default edit policy for newly created sources.'),
+        'template' => NuanceSourcePHIDType::TYPECONST,
       ),
       NuanceSourceManageCapability::CAPABILITY => array(),
     );
   }
 
 }
diff --git a/src/applications/nuance/source/__tests__/NuanceSourceDefinitionTestCase.php b/src/applications/nuance/source/__tests__/NuanceSourceDefinitionTestCase.php
new file mode 100644
index 000000000..39b0358f4
--- /dev/null
+++ b/src/applications/nuance/source/__tests__/NuanceSourceDefinitionTestCase.php
@@ -0,0 +1,10 @@
+<?php
+
+final class NuanceSourceDefinitionTestCase extends PhabricatorTestCase {
+
+  public function testGetAllTypes() {
+    NuanceSourceDefinition::getAllDefinitions();
+    $this->assertTrue(true);
+  }
+
+}
diff --git a/src/applications/oauthserver/PhabricatorOAuthServer.php b/src/applications/oauthserver/PhabricatorOAuthServer.php
index 5f75083c8..e1b9f516b 100644
--- a/src/applications/oauthserver/PhabricatorOAuthServer.php
+++ b/src/applications/oauthserver/PhabricatorOAuthServer.php
@@ -1,272 +1,272 @@
 <?php
 
 /**
  * Implements core OAuth 2.0 Server logic.
  *
  * This class should be used behind business logic that parses input to
  * determine pertinent @{class:PhabricatorUser} $user,
  * @{class:PhabricatorOAuthServerClient} $client(s),
  * @{class:PhabricatorOAuthServerAuthorizationCode} $code(s), and.
  * @{class:PhabricatorOAuthServerAccessToken} $token(s).
  *
  * For an OAuth 2.0 server, there are two main steps:
  *
  * 1) Authorization - the user authorizes a given client to access the data
  * the OAuth 2.0 server protects. Once this is achieved / if it has
  * been achived already, the OAuth server sends the client an authorization
  * code.
  * 2) Access Token - the client should send the authorization code received in
  * step 1 along with its id and secret to the OAuth server to receive an
  * access token. This access token can later be used to access Phabricator
  * data on behalf of the user.
  *
  * @task auth Authorizing @{class:PhabricatorOAuthServerClient}s and
  *            generating @{class:PhabricatorOAuthServerAuthorizationCode}s
  * @task token Validating @{class:PhabricatorOAuthServerAuthorizationCode}s
  *             and generating @{class:PhabricatorOAuthServerAccessToken}s
  * @task internal Internals
  */
-final class PhabricatorOAuthServer {
+final class PhabricatorOAuthServer extends Phobject {
 
   const AUTHORIZATION_CODE_TIMEOUT = 300;
   const ACCESS_TOKEN_TIMEOUT       = 3600;
 
   private $user;
   private $client;
 
   private function getUser() {
     if (!$this->user) {
       throw new PhutilInvalidStateException('setUser');
     }
     return $this->user;
   }
 
   public function setUser(PhabricatorUser $user) {
     $this->user = $user;
     return $this;
   }
 
   private function getClient() {
     if (!$this->client) {
       throw new PhutilInvalidStateException('setClient');
     }
     return $this->client;
   }
 
   public function setClient(PhabricatorOAuthServerClient $client) {
     $this->client = $client;
     return $this;
   }
 
   /**
    * @task auth
    * @return tuple <bool hasAuthorized, ClientAuthorization or null>
    */
   public function userHasAuthorizedClient(array $scope) {
 
     $authorization = id(new PhabricatorOAuthClientAuthorization())->
       loadOneWhere(
         'userPHID = %s AND clientPHID = %s',
         $this->getUser()->getPHID(),
         $this->getClient()->getPHID());
     if (empty($authorization)) {
       return array(false, null);
     }
 
     if ($scope) {
       $missing_scope = array_diff_key($scope, $authorization->getScope());
     } else {
       $missing_scope = false;
     }
 
     if ($missing_scope) {
       return array(false, $authorization);
     }
 
     return array(true, $authorization);
   }
 
   /**
    * @task auth
    */
   public function authorizeClient(array $scope) {
     $authorization = new PhabricatorOAuthClientAuthorization();
     $authorization->setUserPHID($this->getUser()->getPHID());
     $authorization->setClientPHID($this->getClient()->getPHID());
     $authorization->setScope($scope);
     $authorization->save();
 
     return $authorization;
   }
 
   /**
    * @task auth
    */
   public function generateAuthorizationCode(PhutilURI $redirect_uri) {
 
     $code   = Filesystem::readRandomCharacters(32);
     $client = $this->getClient();
 
     $authorization_code = new PhabricatorOAuthServerAuthorizationCode();
     $authorization_code->setCode($code);
     $authorization_code->setClientPHID($client->getPHID());
     $authorization_code->setClientSecret($client->getSecret());
     $authorization_code->setUserPHID($this->getUser()->getPHID());
     $authorization_code->setRedirectURI((string)$redirect_uri);
     $authorization_code->save();
 
     return $authorization_code;
   }
 
   /**
    * @task token
    */
   public function generateAccessToken() {
 
     $token = Filesystem::readRandomCharacters(32);
 
     $access_token = new PhabricatorOAuthServerAccessToken();
     $access_token->setToken($token);
     $access_token->setUserPHID($this->getUser()->getPHID());
     $access_token->setClientPHID($this->getClient()->getPHID());
     $access_token->save();
 
     return $access_token;
   }
 
   /**
    * @task token
    */
   public function validateAuthorizationCode(
     PhabricatorOAuthServerAuthorizationCode $test_code,
     PhabricatorOAuthServerAuthorizationCode $valid_code) {
 
     // check that all the meta data matches
     if ($test_code->getClientPHID() != $valid_code->getClientPHID()) {
       return false;
     }
     if ($test_code->getClientSecret() != $valid_code->getClientSecret()) {
       return false;
     }
 
     // check that the authorization code hasn't timed out
     $created_time = $test_code->getDateCreated();
     $must_be_used_by = $created_time + self::AUTHORIZATION_CODE_TIMEOUT;
     return (time() < $must_be_used_by);
   }
 
   /**
    * @task token
    */
   public function validateAccessToken(
     PhabricatorOAuthServerAccessToken $token,
     $required_scope) {
 
     $created_time    = $token->getDateCreated();
     $must_be_used_by = $created_time + self::ACCESS_TOKEN_TIMEOUT;
     $expired         = time() > $must_be_used_by;
     $authorization   = id(new PhabricatorOAuthClientAuthorization())
       ->loadOneWhere(
         'userPHID = %s AND clientPHID = %s',
         $token->getUserPHID(),
         $token->getClientPHID());
 
     if (!$authorization) {
       return false;
     }
     $token_scope = $authorization->getScope();
     if (!isset($token_scope[$required_scope])) {
       return false;
     }
 
     $valid = true;
     if ($expired) {
       $valid = false;
       // check if the scope includes "offline_access", which makes the
       // token valid despite being expired
       if (isset(
         $token_scope[PhabricatorOAuthServerScope::SCOPE_OFFLINE_ACCESS])) {
         $valid = true;
       }
     }
 
     return $valid;
   }
 
   /**
    * See http://tools.ietf.org/html/draft-ietf-oauth-v2-23#section-3.1.2
    * for details on what makes a given redirect URI "valid".
    */
   public function validateRedirectURI(PhutilURI $uri) {
     if (!PhabricatorEnv::isValidRemoteURIForLink($uri)) {
       return false;
     }
 
     if ($uri->getFragment()) {
       return false;
     }
 
     if (!$uri->getDomain()) {
       return false;
     }
 
     return true;
   }
 
   /**
    * If there's a URI specified in an OAuth request, it must be validated in
    * its own right. Further, it must have the same domain, the same path, the
    * same port, and (at least) the same query parameters as the primary URI.
    */
   public function validateSecondaryRedirectURI(
     PhutilURI $secondary_uri,
     PhutilURI $primary_uri) {
 
     // The secondary URI must be valid.
     if (!$this->validateRedirectURI($secondary_uri)) {
       return false;
     }
 
     // Both URIs must point at the same domain.
     if ($secondary_uri->getDomain() != $primary_uri->getDomain()) {
       return false;
     }
 
     // Both URIs must have the same path
     if ($secondary_uri->getPath() != $primary_uri->getPath()) {
       return false;
     }
 
     // Both URIs must have the same port
     if ($secondary_uri->getPort() != $primary_uri->getPort()) {
       return false;
     }
 
     // Any query parameters present in the first URI must be exactly present
     // in the second URI.
     $need_params = $primary_uri->getQueryParams();
     $have_params = $secondary_uri->getQueryParams();
 
     foreach ($need_params as $key => $value) {
       if (!array_key_exists($key, $have_params)) {
         return false;
       }
       if ((string)$have_params[$key] != (string)$value) {
         return false;
       }
     }
 
     // If the first URI is HTTPS, the second URI must also be HTTPS. This
     // defuses an attack where a third party with control over the network
     // tricks you into using HTTP to authenticate over a link which is supposed
     // to be HTTPS only and sniffs all your token cookies.
     if (strtolower($primary_uri->getProtocol()) == 'https') {
       if (strtolower($secondary_uri->getProtocol()) != 'https') {
         return false;
       }
     }
 
     return true;
   }
 
 }
diff --git a/src/applications/oauthserver/PhabricatorOAuthServerScope.php b/src/applications/oauthserver/PhabricatorOAuthServerScope.php
index 146293dfd..4ab6c455d 100644
--- a/src/applications/oauthserver/PhabricatorOAuthServerScope.php
+++ b/src/applications/oauthserver/PhabricatorOAuthServerScope.php
@@ -1,127 +1,127 @@
 <?php
 
-final class PhabricatorOAuthServerScope {
+final class PhabricatorOAuthServerScope extends Phobject {
 
   const SCOPE_OFFLINE_ACCESS = 'offline_access';
   const SCOPE_WHOAMI         = 'whoami';
   const SCOPE_NOT_ACCESSIBLE = 'not_accessible';
 
   /*
    * Note this does not contain SCOPE_NOT_ACCESSIBLE which is magic
    * used to simplify code for data that is not currently accessible
    * via OAuth.
    */
   public static function getScopesDict() {
     return array(
       self::SCOPE_OFFLINE_ACCESS => 1,
       self::SCOPE_WHOAMI         => 1,
     );
   }
 
   public static function getDefaultScope() {
     return self::SCOPE_WHOAMI;
   }
 
   public static function getCheckboxControl(
     array $current_scopes) {
 
     $have_options = false;
     $scopes = self::getScopesDict();
     $scope_keys = array_keys($scopes);
     sort($scope_keys);
     $default_scope = self::getDefaultScope();
 
     $checkboxes = new AphrontFormCheckboxControl();
     foreach ($scope_keys as $scope) {
       if ($scope == $default_scope) {
         continue;
       }
       if (!isset($current_scopes[$scope])) {
         continue;
       }
 
       $checkboxes->addCheckbox(
         $name = $scope,
         $value = 1,
         $label = self::getCheckboxLabel($scope),
         $checked = isset($current_scopes[$scope]));
       $have_options = true;
     }
 
     if ($have_options) {
       $checkboxes->setLabel(pht('Scope'));
       return $checkboxes;
     }
 
     return null;
   }
 
   private static function getCheckboxLabel($scope) {
     $label = null;
     switch ($scope) {
       case self::SCOPE_OFFLINE_ACCESS:
         $label = pht('Make access tokens granted to this client never expire.');
         break;
       case self::SCOPE_WHOAMI:
         $label = pht('Read access to Conduit method %s.', 'user.whoami');
         break;
     }
 
     return $label;
   }
 
   public static function getScopesFromRequest(AphrontRequest $request) {
     $scopes = self::getScopesDict();
     $requested_scopes = array();
     foreach ($scopes as $scope => $bit) {
       if ($request->getBool($scope)) {
         $requested_scopes[$scope] = 1;
       }
     }
     $requested_scopes[self::getDefaultScope()] = 1;
     return $requested_scopes;
   }
 
   /**
    * A scopes list is considered valid if each scope is a known scope
    * and each scope is seen only once. Otherwise, the list is invalid.
    */
   public static function validateScopesList($scope_list) {
     $scopes       = explode(' ', $scope_list);
     $known_scopes = self::getScopesDict();
     $seen_scopes  = array();
     foreach ($scopes as $scope) {
       if (!isset($known_scopes[$scope])) {
         return false;
       }
       if (isset($seen_scopes[$scope])) {
         return false;
       }
       $seen_scopes[$scope] = 1;
     }
 
     return true;
   }
 
   /**
    * A scopes dictionary is considered valid if each key is a known scope.
    * Otherwise, the dictionary is invalid.
    */
   public static function validateScopesDict($scope_dict) {
     $known_scopes   = self::getScopesDict();
     $unknown_scopes = array_diff_key($scope_dict,
                                      $known_scopes);
     return empty($unknown_scopes);
   }
 
   /**
    * Transforms a space-delimited scopes list into a scopes dict. The list
    * should be validated by @{method:validateScopesList} before
    * transformation.
    */
    public static function scopesListToDict($scope_list) {
     $scopes = explode(' ', $scope_list);
     return array_fill_keys($scopes, 1);
   }
 
 }
diff --git a/src/applications/owners/query/PhabricatorOwnerPathQuery.php b/src/applications/owners/query/PhabricatorOwnerPathQuery.php
index 0e13b21be..7b928b0d3 100644
--- a/src/applications/owners/query/PhabricatorOwnerPathQuery.php
+++ b/src/applications/owners/query/PhabricatorOwnerPathQuery.php
@@ -1,32 +1,32 @@
 <?php
 
-final class PhabricatorOwnerPathQuery {
+final class PhabricatorOwnerPathQuery extends Phobject {
 
   public static function loadAffectedPaths(
     PhabricatorRepository $repository,
     PhabricatorRepositoryCommit $commit,
     PhabricatorUser $user) {
 
     $drequest = DiffusionRequest::newFromDictionary(
       array(
         'user'        => $user,
         'repository'  => $repository,
         'commit'      => $commit->getCommitIdentifier(),
       ));
 
     $path_query = DiffusionPathChangeQuery::newFromDiffusionRequest(
       $drequest);
     $paths = $path_query->loadChanges();
 
     $result = array();
     foreach ($paths as $path) {
       $basic_path = '/'.$path->getPath();
       if ($path->getFileType() == DifferentialChangeType::FILE_DIRECTORY) {
         $basic_path = rtrim($basic_path, '/').'/';
       }
       $result[] = $basic_path;
     }
     return $result;
   }
 
 }
diff --git a/src/applications/passphrase/conduit/PassphraseQueryConduitAPIMethod.php b/src/applications/passphrase/conduit/PassphraseQueryConduitAPIMethod.php
index 0163cd504..18010efd7 100644
--- a/src/applications/passphrase/conduit/PassphraseQueryConduitAPIMethod.php
+++ b/src/applications/passphrase/conduit/PassphraseQueryConduitAPIMethod.php
@@ -1,132 +1,132 @@
 <?php
 
 final class PassphraseQueryConduitAPIMethod
   extends PassphraseConduitAPIMethod {
 
   public function getAPIMethodName() {
     return 'passphrase.query';
   }
 
   public function getMethodDescription() {
     return pht('Query credentials.');
   }
 
   public function newQueryObject() {
     return new PassphraseCredentialQuery();
   }
 
   protected function defineParamTypes() {
     return array(
       'ids' => 'optional list<int>',
       'phids' => 'optional list<phid>',
       'needSecrets' => 'optional bool',
       'needPublicKeys' => 'optional bool',
     );
   }
 
   protected function defineReturnType() {
     return 'list<dict>';
   }
 
   protected function execute(ConduitAPIRequest $request) {
     $query = $this->newQueryForRequest($request);
 
     if ($request->getValue('ids')) {
       $query->withIDs($request->getValue('ids'));
     }
 
     if ($request->getValue('phids')) {
       $query->withPHIDs($request->getValue('phids'));
     }
 
     if ($request->getValue('needSecrets')) {
       $query->needSecrets(true);
     }
 
     $pager = $this->newPager($request);
     $credentials = $query->executeWithCursorPager($pager);
 
     $results = array();
     foreach ($credentials as $credential) {
       $type = PassphraseCredentialType::getTypeByConstant(
         $credential->getCredentialType());
       if (!$type) {
         continue;
       }
 
       $public_key = null;
       if ($request->getValue('needPublicKeys') && $type->hasPublicKey()) {
         $public_key = $type->getPublicKey(
           $request->getUser(),
           $credential);
       }
 
       $material = array();
 
       $secret = null;
       if ($request->getValue('needSecrets')) {
         if ($credential->getAllowConduit()) {
           $secret = $credential->getSecret();
           if ($secret) {
             $secret = $secret->openEnvelope();
           } else {
             $material['destroyed'] = pht(
               'The private material for this credential has been '.
               'destroyed.');
           }
         }
       }
 
       switch ($credential->getCredentialType()) {
-        case PassphraseCredentialTypeSSHPrivateKeyFile::CREDENTIAL_TYPE:
+        case PassphraseSSHPrivateKeyFileCredentialType::CREDENTIAL_TYPE:
           if ($secret) {
             $material['file'] = $secret;
           }
           if ($public_key) {
             $material['publicKey'] = $public_key;
           }
           break;
-        case PassphraseCredentialTypeSSHGeneratedKey::CREDENTIAL_TYPE:
-        case PassphraseCredentialTypeSSHPrivateKeyText::CREDENTIAL_TYPE:
+        case PassphraseSSHGeneratedKeyCredentialType::CREDENTIAL_TYPE:
+        case PassphraseSSHPrivateKeyTextCredentialType::CREDENTIAL_TYPE:
           if ($secret) {
             $material['privateKey'] = $secret;
           }
           if ($public_key) {
             $material['publicKey'] = $public_key;
           }
           break;
-        case PassphraseCredentialTypePassword::CREDENTIAL_TYPE:
+        case PassphrasePasswordCredentialType::CREDENTIAL_TYPE:
           if ($secret) {
             $material['password'] = $secret;
           }
           break;
       }
 
       if (!$credential->getAllowConduit()) {
         $material['noAPIAccess'] = pht(
           'This private material for this credential is not accessible via '.
           'API calls.');
       }
 
       $results[$credential->getPHID()] = array(
         'id' => $credential->getID(),
         'phid' => $credential->getPHID(),
         'type' => $credential->getCredentialType(),
         'name' => $credential->getName(),
         'description' => $credential->getDescription(),
         'uri' =>
           PhabricatorEnv::getProductionURI('/'.$credential->getMonogram()),
         'monogram' => $credential->getMonogram(),
         'username' => $credential->getUsername(),
         'material' => $material,
       );
     }
 
     $result = array(
       'data' => $results,
     );
 
     return $this->addPagerResults($result, $pager);
   }
 
 }
diff --git a/src/applications/passphrase/credentialtype/PassphraseCredentialTypePassword.php b/src/applications/passphrase/credentialtype/PassphrasePasswordCredentialType.php
similarity index 93%
rename from src/applications/passphrase/credentialtype/PassphraseCredentialTypePassword.php
rename to src/applications/passphrase/credentialtype/PassphrasePasswordCredentialType.php
index bc55f1e4d..695059fd1 100644
--- a/src/applications/passphrase/credentialtype/PassphraseCredentialTypePassword.php
+++ b/src/applications/passphrase/credentialtype/PassphrasePasswordCredentialType.php
@@ -1,34 +1,34 @@
 <?php
 
-final class PassphraseCredentialTypePassword
+final class PassphrasePasswordCredentialType
   extends PassphraseCredentialType {
 
   const CREDENTIAL_TYPE = 'password';
   const PROVIDES_TYPE = 'provides/password';
 
   public function getCredentialType() {
     return self::CREDENTIAL_TYPE;
   }
 
   public function getProvidesType() {
     return self::PROVIDES_TYPE;
   }
 
   public function getCredentialTypeName() {
     return pht('Password');
   }
 
   public function getCredentialTypeDescription() {
     return pht('Store a plaintext password.');
   }
 
   public function getSecretLabel() {
     return pht('Password');
   }
 
   public function newSecretControl() {
     return id(new AphrontFormPasswordControl())
       ->setDisableAutocomplete(true);
   }
 
 }
diff --git a/src/applications/passphrase/credentialtype/PassphraseCredentialTypeSSHGeneratedKey.php b/src/applications/passphrase/credentialtype/PassphraseSSHGeneratedKeyCredentialType.php
similarity index 87%
rename from src/applications/passphrase/credentialtype/PassphraseCredentialTypeSSHGeneratedKey.php
rename to src/applications/passphrase/credentialtype/PassphraseSSHGeneratedKeyCredentialType.php
index 4deff95f5..a0be345f5 100644
--- a/src/applications/passphrase/credentialtype/PassphraseCredentialTypeSSHGeneratedKey.php
+++ b/src/applications/passphrase/credentialtype/PassphraseSSHGeneratedKeyCredentialType.php
@@ -1,36 +1,36 @@
 <?php
 
-final class PassphraseCredentialTypeSSHGeneratedKey
-  extends PassphraseCredentialTypeSSHPrivateKey {
+final class PassphraseSSHGeneratedKeyCredentialType
+  extends PassphraseSSHPrivateKeyCredentialType {
 
   const CREDENTIAL_TYPE = 'ssh-generated-key';
 
   public function getCredentialType() {
     return self::CREDENTIAL_TYPE;
   }
 
   public function getCredentialTypeName() {
     return pht('SSH Private Key (Generated)');
   }
 
   public function getCredentialTypeDescription() {
     return pht('Generate an SSH keypair.');
   }
 
   public function getSecretLabel() {
     return pht('Generated Key');
   }
 
   public function didInitializeNewCredential(
     PhabricatorUser $actor,
     PassphraseCredential $credential) {
 
     $pair = PhabricatorSSHKeyGenerator::generateKeypair();
     list($public_key, $private_key) = $pair;
 
     $credential->attachSecret(new PhutilOpaqueEnvelope($private_key));
 
     return $credential;
   }
 
 }
diff --git a/src/applications/passphrase/credentialtype/PassphraseCredentialTypeSSHPrivateKey.php b/src/applications/passphrase/credentialtype/PassphraseSSHPrivateKeyCredentialType.php
similarity index 91%
rename from src/applications/passphrase/credentialtype/PassphraseCredentialTypeSSHPrivateKey.php
rename to src/applications/passphrase/credentialtype/PassphraseSSHPrivateKeyCredentialType.php
index 1c874be45..d8986d573 100644
--- a/src/applications/passphrase/credentialtype/PassphraseCredentialTypeSSHPrivateKey.php
+++ b/src/applications/passphrase/credentialtype/PassphraseSSHPrivateKeyCredentialType.php
@@ -1,28 +1,28 @@
 <?php
 
-abstract class PassphraseCredentialTypeSSHPrivateKey
+abstract class PassphraseSSHPrivateKeyCredentialType
   extends PassphraseCredentialType {
 
   const PROVIDES_TYPE = 'provides/ssh-key-file';
 
   final public function getProvidesType() {
     return self::PROVIDES_TYPE;
   }
 
   public function hasPublicKey() {
     return true;
   }
 
   public function getPublicKey(
     PhabricatorUser $viewer,
     PassphraseCredential $credential) {
 
     $key = PassphraseSSHKey::loadFromPHID($credential->getPHID(), $viewer);
     $file = $key->getKeyfileEnvelope();
 
     list($stdout) = execx('ssh-keygen -y -f %P', $file);
 
     return $stdout;
   }
 
 }
diff --git a/src/applications/passphrase/credentialtype/PassphraseCredentialTypeSSHPrivateKeyFile.php b/src/applications/passphrase/credentialtype/PassphraseSSHPrivateKeyFileCredentialType.php
similarity index 90%
rename from src/applications/passphrase/credentialtype/PassphraseCredentialTypeSSHPrivateKeyFile.php
rename to src/applications/passphrase/credentialtype/PassphraseSSHPrivateKeyFileCredentialType.php
index 66dd36e6c..9b05a25cf 100644
--- a/src/applications/passphrase/credentialtype/PassphraseCredentialTypeSSHPrivateKeyFile.php
+++ b/src/applications/passphrase/credentialtype/PassphraseSSHPrivateKeyFileCredentialType.php
@@ -1,41 +1,41 @@
 <?php
 
-final class PassphraseCredentialTypeSSHPrivateKeyFile
-  extends PassphraseCredentialTypeSSHPrivateKey {
+final class PassphraseSSHPrivateKeyFileCredentialType
+  extends PassphraseSSHPrivateKeyCredentialType {
 
   const CREDENTIAL_TYPE = 'ssh-key-file';
 
   public function getCredentialType() {
     return self::CREDENTIAL_TYPE;
   }
 
   public function getCredentialTypeName() {
     return pht('SSH Private Key File');
   }
 
   public function getCredentialTypeDescription() {
     return pht('Store the path on disk to an SSH private key.');
   }
 
   public function getSecretLabel() {
     return pht('Path On Disk');
   }
 
   public function newSecretControl() {
     return new AphrontFormTextControl();
   }
 
   public function isCreateable() {
     // This credential type exists to support historic repository configuration.
     // We don't support creating new credentials with this type, since it does
     // not scale and managing passwords is much more difficult than if we have
     // the key text.
     return false;
   }
 
   public function hasPublicKey() {
     // These have public keys, but they'd be cumbersome to extract.
     return true;
   }
 
 }
diff --git a/src/applications/passphrase/credentialtype/PassphraseCredentialTypeSSHPrivateKeyText.php b/src/applications/passphrase/credentialtype/PassphraseSSHPrivateKeyTextCredentialType.php
similarity index 93%
rename from src/applications/passphrase/credentialtype/PassphraseCredentialTypeSSHPrivateKeyText.php
rename to src/applications/passphrase/credentialtype/PassphraseSSHPrivateKeyTextCredentialType.php
index c197d7eae..d573f19f5 100644
--- a/src/applications/passphrase/credentialtype/PassphraseCredentialTypeSSHPrivateKeyText.php
+++ b/src/applications/passphrase/credentialtype/PassphraseSSHPrivateKeyTextCredentialType.php
@@ -1,68 +1,68 @@
 <?php
 
-final class PassphraseCredentialTypeSSHPrivateKeyText
-  extends PassphraseCredentialTypeSSHPrivateKey {
+final class PassphraseSSHPrivateKeyTextCredentialType
+  extends PassphraseSSHPrivateKeyCredentialType {
 
   const CREDENTIAL_TYPE = 'ssh-key-text';
 
   public function getCredentialType() {
     return self::CREDENTIAL_TYPE;
   }
 
   public function getCredentialTypeName() {
     return pht('SSH Private Key');
   }
 
   public function getCredentialTypeDescription() {
     return pht('Store the plaintext of an SSH private key.');
   }
 
   public function getSecretLabel() {
     return pht('Private Key');
   }
 
   public function shouldShowPasswordField() {
     return true;
   }
 
   public function getPasswordLabel() {
     return pht('Password for Key');
   }
 
   public function requiresPassword(PhutilOpaqueEnvelope $secret) {
     // According to the internet, this is the canonical test for an SSH private
     // key with a password.
     return preg_match('/ENCRYPTED/', $secret->openEnvelope());
   }
 
   public function decryptSecret(
     PhutilOpaqueEnvelope $secret,
     PhutilOpaqueEnvelope $password) {
 
     $tmp = new TempFile();
     Filesystem::writeFile($tmp, $secret->openEnvelope());
 
     if (!Filesystem::binaryExists('ssh-keygen')) {
       throw new Exception(
         pht(
           'Decrypting SSH keys requires the `%s` binary, but it '.
           'is not available in %s. Either make it available or strip the '.
           'password fromt his SSH key manually before uploading it.',
           'ssh-keygen',
           '$PATH'));
     }
 
     list($err, $stdout, $stderr) = exec_manual(
       'ssh-keygen -p -P %P -N %s -f %s',
       $password,
       '',
       (string)$tmp);
 
     if ($err) {
       return null;
     } else {
       return new PhutilOpaqueEnvelope(Filesystem::readFile($tmp));
     }
   }
 
 }
diff --git a/src/applications/passphrase/credentialtype/__tests__/PassphraseCredentialTypeTestCase.php b/src/applications/passphrase/credentialtype/__tests__/PassphraseCredentialTypeTestCase.php
new file mode 100644
index 000000000..0642ad132
--- /dev/null
+++ b/src/applications/passphrase/credentialtype/__tests__/PassphraseCredentialTypeTestCase.php
@@ -0,0 +1,10 @@
+<?php
+
+final class PassphraseCredentialTypeTestCase extends PhabricatorTestCase {
+
+  public function testGetAllTypes() {
+    PassphraseCredentialType::getAllTypes();
+    $this->assertTrue(true);
+  }
+
+}
diff --git a/src/applications/passphrase/keys/PassphrasePasswordKey.php b/src/applications/passphrase/keys/PassphrasePasswordKey.php
index df2e683c2..ef4cc7a28 100644
--- a/src/applications/passphrase/keys/PassphrasePasswordKey.php
+++ b/src/applications/passphrase/keys/PassphrasePasswordKey.php
@@ -1,17 +1,17 @@
 <?php
 
 final class PassphrasePasswordKey extends PassphraseAbstractKey {
 
   public static function loadFromPHID($phid, PhabricatorUser $viewer) {
     $key = new PassphrasePasswordKey();
     return $key->loadAndValidateFromPHID(
       $phid,
       $viewer,
-      PassphraseCredentialTypePassword::PROVIDES_TYPE);
+      PassphrasePasswordCredentialType::PROVIDES_TYPE);
   }
 
   public function getPasswordEnvelope() {
     return $this->requireCredential()->getSecret();
   }
 
 }
diff --git a/src/applications/passphrase/keys/PassphraseSSHKey.php b/src/applications/passphrase/keys/PassphraseSSHKey.php
index 0790d5342..f55eb3be9 100644
--- a/src/applications/passphrase/keys/PassphraseSSHKey.php
+++ b/src/applications/passphrase/keys/PassphraseSSHKey.php
@@ -1,45 +1,45 @@
 <?php
 
 final class PassphraseSSHKey extends PassphraseAbstractKey {
 
   private $keyFile;
 
   public static function loadFromPHID($phid, PhabricatorUser $viewer) {
     $key = new PassphraseSSHKey();
     return $key->loadAndValidateFromPHID(
       $phid,
       $viewer,
-      PassphraseCredentialTypeSSHPrivateKey::PROVIDES_TYPE);
+      PassphraseSSHPrivateKeyCredentialType::PROVIDES_TYPE);
   }
 
   public function getKeyfileEnvelope() {
     $credential = $this->requireCredential();
 
-    $file_type = PassphraseCredentialTypeSSHPrivateKeyFile::CREDENTIAL_TYPE;
+    $file_type = PassphraseSSHPrivateKeyFileCredentialType::CREDENTIAL_TYPE;
     if ($credential->getCredentialType() != $file_type) {
       // If the credential does not store a file, write the key text out to a
       // temporary file so we can pass it to `ssh`.
       if (!$this->keyFile) {
         $secret = $credential->getSecret();
         if (!$secret) {
           throw new Exception(
             pht(
               'Attempting to use a credential ("%s") but the credential '.
               'secret has been destroyed!',
               $credential->getMonogram()));
         }
 
         $temporary_file = new TempFile('passphrase-ssh-key');
         Filesystem::changePermissions($temporary_file, 0600);
         Filesystem::writeFile($temporary_file, $secret->openEnvelope());
 
         $this->keyFile = $temporary_file;
       }
 
       return new PhutilOpaqueEnvelope((string)$this->keyFile);
     }
 
     return $credential->getSecret();
   }
 
 }
diff --git a/src/applications/paste/application/PhabricatorPasteApplication.php b/src/applications/paste/application/PhabricatorPasteApplication.php
index c0afe865b..f9cd03dd7 100644
--- a/src/applications/paste/application/PhabricatorPasteApplication.php
+++ b/src/applications/paste/application/PhabricatorPasteApplication.php
@@ -1,99 +1,101 @@
 <?php
 
 final class PhabricatorPasteApplication extends PhabricatorApplication {
 
   public function getName() {
     return pht('Paste');
   }
 
   public function getBaseURI() {
     return '/paste/';
   }
 
   public function getFontIcon() {
     return 'fa-paste';
   }
 
   public function getTitleGlyph() {
     return "\xE2\x9C\x8E";
   }
 
   public function getApplicationGroup() {
     return self::GROUP_UTILITIES;
   }
 
   public function getShortDescription() {
     return pht('Share Text Snippets');
   }
 
   public function getRemarkupRules() {
     return array(
       new PhabricatorPasteRemarkupRule(),
     );
   }
 
   public function getRoutes() {
     return array(
       '/P(?P<id>[1-9]\d*)(?:\$(?P<lines>\d+(?:-\d+)?))?'
         => 'PhabricatorPasteViewController',
       '/paste/' => array(
         '(query/(?P<queryKey>[^/]+)/)?' => 'PhabricatorPasteListController',
         'create/' => 'PhabricatorPasteEditController',
         'edit/(?P<id>[1-9]\d*)/' => 'PhabricatorPasteEditController',
         'comment/(?P<id>[1-9]\d*)/' => 'PhabricatorPasteCommentController',
       ),
     );
   }
 
   public function supportsEmailIntegration() {
     return true;
   }
 
   public function getAppEmailBlurb() {
     return pht(
       'Send email to these addresses to create pastes. %s',
       phutil_tag(
         'a',
         array(
           'href' => $this->getInboundEmailSupportLink(),
         ),
         pht('Learn More')));
   }
 
   protected function getCustomCapabilities() {
     return array(
       PasteDefaultViewCapability::CAPABILITY => array(
         'caption' => pht('Default view policy for newly created pastes.'),
+        'template' => PhabricatorPastePastePHIDType::TYPECONST,
       ),
       PasteDefaultEditCapability::CAPABILITY => array(
         'caption' => pht('Default edit policy for newly created pastes.'),
+        'template' => PhabricatorPastePastePHIDType::TYPECONST,
       ),
     );
   }
 
   public function getQuickCreateItems(PhabricatorUser $viewer) {
     $items = array();
 
     $item = id(new PHUIListItemView())
       ->setName(pht('Paste'))
       ->setIcon('fa-clipboard')
       ->setHref($this->getBaseURI().'create/');
     $items[] = $item;
 
     return $items;
   }
 
   public function getMailCommandObjects() {
     return array(
       'paste' => array(
         'name' => pht('Email Commands: Pastes'),
         'header' => pht('Interacting with Pastes'),
         'object' => new PhabricatorPaste(),
         'summary' => pht(
           'This page documents the commands you can use to interact with '.
           'pastes.'),
       ),
     );
   }
 
 }
diff --git a/src/applications/paste/controller/PhabricatorPasteEditController.php b/src/applications/paste/controller/PhabricatorPasteEditController.php
index 7aa28673d..dcc490095 100644
--- a/src/applications/paste/controller/PhabricatorPasteEditController.php
+++ b/src/applications/paste/controller/PhabricatorPasteEditController.php
@@ -1,249 +1,244 @@
 <?php
 
 final class PhabricatorPasteEditController extends PhabricatorPasteController {
 
   private $id;
 
   public function willProcessRequest(array $data) {
     $this->id = idx($data, 'id');
   }
 
   public function processRequest() {
     $request = $this->getRequest();
     $user = $request->getUser();
 
     $parent = null;
     $parent_id = null;
     if (!$this->id) {
       $is_create = true;
 
       $paste = PhabricatorPaste::initializeNewPaste($user);
 
       $parent_id = $request->getStr('parent');
       if ($parent_id) {
         // NOTE: If the Paste is forked from a paste which the user no longer
         // has permission to see, we still let them edit it.
         $parent = id(new PhabricatorPasteQuery())
           ->setViewer($user)
           ->withIDs(array($parent_id))
           ->needContent(true)
           ->needRawContent(true)
           ->execute();
         $parent = head($parent);
 
         if ($parent) {
           $paste->setParentPHID($parent->getPHID());
           $paste->setViewPolicy($parent->getViewPolicy());
         }
       }
 
       $paste->setAuthorPHID($user->getPHID());
       $paste->attachRawContent('');
     } else {
       $is_create = false;
 
       $paste = id(new PhabricatorPasteQuery())
         ->setViewer($user)
         ->requireCapabilities(
           array(
             PhabricatorPolicyCapability::CAN_VIEW,
             PhabricatorPolicyCapability::CAN_EDIT,
           ))
         ->withIDs(array($this->id))
         ->needRawContent(true)
         ->executeOne();
       if (!$paste) {
         return new Aphront404Response();
       }
     }
 
     $v_space = $paste->getSpacePHID();
     if ($is_create && $parent) {
       $v_title = pht('Fork of %s', $parent->getFullName());
       $v_language = $parent->getLanguage();
       $v_text = $parent->getRawContent();
       $v_space = $parent->getSpacePHID();
     } else {
       $v_title = $paste->getTitle();
       $v_language = $paste->getLanguage();
       $v_text = $paste->getRawContent();
     }
     $v_view_policy = $paste->getViewPolicy();
     $v_edit_policy = $paste->getEditPolicy();
 
     if ($is_create) {
       $v_projects = array();
     } else {
       $v_projects = PhabricatorEdgeQuery::loadDestinationPHIDs(
         $paste->getPHID(),
         PhabricatorProjectObjectHasProjectEdgeType::EDGECONST);
       $v_projects = array_reverse($v_projects);
     }
 
     $validation_exception = null;
     if ($request->isFormPost()) {
       $xactions = array();
 
       $v_text = $request->getStr('text');
       $v_title = $request->getStr('title');
       $v_language = $request->getStr('language');
       $v_view_policy = $request->getStr('can_view');
       $v_edit_policy = $request->getStr('can_edit');
       $v_projects = $request->getArr('projects');
       $v_space = $request->getStr('spacePHID');
 
       // NOTE: The author is the only editor and can always view the paste,
       // so it's impossible for them to choose an invalid policy.
 
       if ($is_create || ($v_text !== $paste->getRawContent())) {
         $file = PhabricatorPasteEditor::initializeFileForPaste(
           $user,
           $v_title,
           $v_text);
 
         $xactions[] = id(new PhabricatorPasteTransaction())
           ->setTransactionType(PhabricatorPasteTransaction::TYPE_CONTENT)
           ->setNewValue($file->getPHID());
       }
 
       $xactions[] = id(new PhabricatorPasteTransaction())
         ->setTransactionType(PhabricatorPasteTransaction::TYPE_TITLE)
         ->setNewValue($v_title);
       $xactions[] = id(new PhabricatorPasteTransaction())
         ->setTransactionType(PhabricatorPasteTransaction::TYPE_LANGUAGE)
         ->setNewValue($v_language);
       $xactions[] = id(new PhabricatorPasteTransaction())
         ->setTransactionType(PhabricatorTransactions::TYPE_VIEW_POLICY)
         ->setNewValue($v_view_policy);
       $xactions[] = id(new PhabricatorPasteTransaction())
         ->setTransactionType(PhabricatorTransactions::TYPE_EDIT_POLICY)
         ->setNewValue($v_edit_policy);
       $xactions[] = id(new PhabricatorPasteTransaction())
         ->setTransactionType(PhabricatorTransactions::TYPE_SPACE)
         ->setNewValue($v_space);
 
       $proj_edge_type = PhabricatorProjectObjectHasProjectEdgeType::EDGECONST;
       $xactions[] = id(new PhabricatorPasteTransaction())
         ->setTransactionType(PhabricatorTransactions::TYPE_EDGE)
         ->setMetadataValue('edge:type', $proj_edge_type)
         ->setNewValue(array('=' => array_fuse($v_projects)));
 
       $editor = id(new PhabricatorPasteEditor())
         ->setActor($user)
         ->setContentSourceFromRequest($request)
         ->setContinueOnNoEffect(true);
 
       try {
         $xactions = $editor->applyTransactions($paste, $xactions);
         return id(new AphrontRedirectResponse())->setURI($paste->getURI());
       } catch (PhabricatorApplicationTransactionValidationException $ex) {
         $validation_exception = $ex;
       }
     }
 
     $form = new AphrontFormView();
 
     $langs = array(
       '' => pht('(Detect From Filename in Title)'),
     ) + PhabricatorEnv::getEnvConfig('pygments.dropdown-choices');
 
     $form
       ->setUser($user)
       ->addHiddenInput('parent', $parent_id)
       ->appendChild(
         id(new AphrontFormTextControl())
           ->setLabel(pht('Title'))
           ->setValue($v_title)
           ->setName('title'))
       ->appendChild(
         id(new AphrontFormSelectControl())
           ->setLabel(pht('Language'))
           ->setName('language')
           ->setValue($v_language)
           ->setOptions($langs));
 
     $policies = id(new PhabricatorPolicyQuery())
       ->setViewer($user)
       ->setObject($paste)
       ->execute();
 
-    $form->appendControl(
-      id(new PhabricatorSpacesControl())
-        ->setObject($paste)
-        ->setValue($v_space)
-        ->setName('spacePHID'));
-
     $form->appendChild(
       id(new AphrontFormPolicyControl())
         ->setUser($user)
         ->setCapability(PhabricatorPolicyCapability::CAN_VIEW)
         ->setPolicyObject($paste)
         ->setPolicies($policies)
         ->setValue($v_view_policy)
+        ->setSpacePHID($v_space)
         ->setName('can_view'));
 
     $form->appendChild(
       id(new AphrontFormPolicyControl())
         ->setUser($user)
         ->setCapability(PhabricatorPolicyCapability::CAN_EDIT)
         ->setPolicyObject($paste)
         ->setPolicies($policies)
         ->setValue($v_edit_policy)
         ->setName('can_edit'));
 
     $form->appendControl(
       id(new AphrontFormTokenizerControl())
         ->setLabel(pht('Projects'))
         ->setName('projects')
         ->setValue($v_projects)
         ->setDatasource(new PhabricatorProjectDatasource()));
 
     $form
       ->appendChild(
         id(new AphrontFormTextAreaControl())
           ->setLabel(pht('Text'))
           ->setValue($v_text)
           ->setHeight(AphrontFormTextAreaControl::HEIGHT_VERY_TALL)
           ->setCustomClass('PhabricatorMonospaced')
           ->setName('text'));
 
     $submit = new AphrontFormSubmitControl();
 
     if (!$is_create) {
       $submit->addCancelButton($paste->getURI());
       $submit->setValue(pht('Save Paste'));
       $title = pht('Edit %s', $paste->getFullName());
       $short = pht('Edit');
     } else {
       $submit->setValue(pht('Create Paste'));
       $title = pht('Create New Paste');
       $short = pht('Create');
     }
 
     $form->appendChild($submit);
 
     $form_box = id(new PHUIObjectBoxView())
       ->setHeaderText($title)
       ->setForm($form);
 
     if ($validation_exception) {
       $form_box->setValidationException($validation_exception);
     }
 
     $crumbs = $this->buildApplicationCrumbs($this->buildSideNavView());
     if (!$is_create) {
       $crumbs->addTextCrumb('P'.$paste->getID(), '/P'.$paste->getID());
     }
     $crumbs->addTextCrumb($short);
 
     return $this->buildApplicationPage(
       array(
         $crumbs,
         $form_box,
       ),
       array(
         'title' => $title,
       ));
   }
 
 }
diff --git a/src/applications/paste/storage/PhabricatorPaste.php b/src/applications/paste/storage/PhabricatorPaste.php
index da129b8cf..661701ba0 100644
--- a/src/applications/paste/storage/PhabricatorPaste.php
+++ b/src/applications/paste/storage/PhabricatorPaste.php
@@ -1,223 +1,224 @@
 <?php
 
 final class PhabricatorPaste extends PhabricatorPasteDAO
   implements
     PhabricatorSubscribableInterface,
     PhabricatorTokenReceiverInterface,
     PhabricatorFlaggableInterface,
     PhabricatorMentionableInterface,
     PhabricatorPolicyInterface,
     PhabricatorProjectInterface,
     PhabricatorDestructibleInterface,
     PhabricatorApplicationTransactionInterface,
     PhabricatorSpacesInterface {
 
   protected $title;
   protected $authorPHID;
   protected $filePHID;
   protected $language;
   protected $parentPHID;
   protected $viewPolicy;
   protected $editPolicy;
   protected $mailKey;
   protected $spacePHID;
 
   private $content = self::ATTACHABLE;
   private $rawContent = self::ATTACHABLE;
 
   public static function initializeNewPaste(PhabricatorUser $actor) {
     $app = id(new PhabricatorApplicationQuery())
       ->setViewer($actor)
       ->withClasses(array('PhabricatorPasteApplication'))
       ->executeOne();
 
     $view_policy = $app->getPolicy(PasteDefaultViewCapability::CAPABILITY);
     $edit_policy = $app->getPolicy(PasteDefaultEditCapability::CAPABILITY);
 
     return id(new PhabricatorPaste())
       ->setTitle('')
       ->setAuthorPHID($actor->getPHID())
       ->setViewPolicy($view_policy)
-      ->setEditPolicy($edit_policy);
+      ->setEditPolicy($edit_policy)
+      ->setSpacePHID($actor->getDefaultSpacePHID());
   }
 
   public function getURI() {
     return '/'.$this->getMonogram();
   }
 
   public function getMonogram() {
     return 'P'.$this->getID();
   }
 
   protected function getConfiguration() {
     return array(
       self::CONFIG_AUX_PHID => true,
       self::CONFIG_COLUMN_SCHEMA => array(
         'title' => 'text255',
         'language' => 'text64',
         'mailKey' => 'bytes20',
         'parentPHID' => 'phid?',
 
         // T6203/NULLABILITY
         // Pastes should always have a view policy.
         'viewPolicy' => 'policy?',
       ),
       self::CONFIG_KEY_SCHEMA => array(
         'parentPHID' => array(
           'columns' => array('parentPHID'),
         ),
         'authorPHID' => array(
           'columns' => array('authorPHID'),
         ),
         'key_dateCreated' => array(
           'columns' => array('dateCreated'),
         ),
         'key_language' => array(
           'columns' => array('language'),
         ),
       ),
     ) + parent::getConfiguration();
   }
 
   public function generatePHID() {
     return PhabricatorPHID::generateNewPHID(
       PhabricatorPastePastePHIDType::TYPECONST);
   }
 
   public function save() {
     if (!$this->getMailKey()) {
       $this->setMailKey(Filesystem::readRandomCharacters(20));
     }
     return parent::save();
   }
 
   public function getFullName() {
     $title = $this->getTitle();
     if (!$title) {
       $title = pht('(An Untitled Masterwork)');
     }
     return 'P'.$this->getID().' '.$title;
   }
 
   public function getContent() {
     return $this->assertAttached($this->content);
   }
 
   public function attachContent($content) {
     $this->content = $content;
     return $this;
   }
 
   public function getRawContent() {
     return $this->assertAttached($this->rawContent);
   }
 
   public function attachRawContent($raw_content) {
     $this->rawContent = $raw_content;
     return $this;
   }
 
 /* -(  PhabricatorSubscribableInterface  )----------------------------------- */
 
 
   public function isAutomaticallySubscribed($phid) {
     return ($this->authorPHID == $phid);
   }
 
   public function shouldShowSubscribersProperty() {
     return true;
   }
 
   public function shouldAllowSubscription($phid) {
     return true;
   }
 
 
 /* -(  PhabricatorTokenReceiverInterface  )---------------------------------- */
 
   public function getUsersToNotifyOfTokenGiven() {
     return array(
       $this->getAuthorPHID(),
     );
   }
 
 
 /* -(  PhabricatorPolicyInterface  )----------------------------------------- */
 
 
   public function getCapabilities() {
     return array(
       PhabricatorPolicyCapability::CAN_VIEW,
       PhabricatorPolicyCapability::CAN_EDIT,
     );
   }
 
   public function getPolicy($capability) {
     if ($capability == PhabricatorPolicyCapability::CAN_VIEW) {
       return $this->viewPolicy;
     } else if ($capability == PhabricatorPolicyCapability::CAN_EDIT) {
       return $this->editPolicy;
     }
     return PhabricatorPolicies::POLICY_NOONE;
   }
 
   public function hasAutomaticCapability($capability, PhabricatorUser $user) {
     return ($user->getPHID() == $this->getAuthorPHID());
   }
 
   public function describeAutomaticCapability($capability) {
     return pht('The author of a paste can always view and edit it.');
   }
 
 
 /* -(  PhabricatorDestructibleInterface  )----------------------------------- */
 
 
   public function destroyObjectPermanently(
     PhabricatorDestructionEngine $engine) {
 
     if ($this->filePHID) {
       $file = id(new PhabricatorFileQuery())
         ->setViewer($engine->getViewer())
         ->withPHIDs(array($this->filePHID))
         ->executeOne();
       if ($file) {
         $engine->destroyObject($file);
       }
     }
 
     $this->delete();
   }
 
 
 /* -(  PhabricatorApplicationTransactionInterface  )------------------------- */
 
 
   public function getApplicationTransactionEditor() {
     return new PhabricatorPasteEditor();
   }
 
   public function getApplicationTransactionObject() {
     return $this;
   }
 
   public function getApplicationTransactionTemplate() {
     return new PhabricatorPasteTransaction();
   }
 
   public function willRenderTimeline(
     PhabricatorApplicationTransactionView $timeline,
     AphrontRequest $request) {
 
     return $timeline;
   }
 
 
 /* -(  PhabricatorSpacesInterface  )----------------------------------------- */
 
 
   public function getSpacePHID() {
     return $this->spacePHID;
   }
 
 }
diff --git a/src/applications/people/storage/PhabricatorUser.php b/src/applications/people/storage/PhabricatorUser.php
index 1d1d3316c..55ade255a 100644
--- a/src/applications/people/storage/PhabricatorUser.php
+++ b/src/applications/people/storage/PhabricatorUser.php
@@ -1,1245 +1,1250 @@
 <?php
 
 /**
  * @task availability Availability
  * @task image-cache Profile Image Cache
  * @task factors Multi-Factor Authentication
  * @task handles Managing Handles
  */
 final class PhabricatorUser
   extends PhabricatorUserDAO
   implements
     PhutilPerson,
     PhabricatorPolicyInterface,
     PhabricatorCustomFieldInterface,
     PhabricatorDestructibleInterface,
     PhabricatorSSHPublicKeyInterface,
     PhabricatorApplicationTransactionInterface {
 
   const SESSION_TABLE = 'phabricator_session';
   const NAMETOKEN_TABLE = 'user_nametoken';
   const MAXIMUM_USERNAME_LENGTH = 64;
 
   protected $userName;
   protected $realName;
   protected $sex;
   protected $translation;
   protected $passwordSalt;
   protected $passwordHash;
   protected $profileImagePHID;
   protected $profileImageCache;
   protected $availabilityCache;
   protected $availabilityCacheTTL;
   protected $timezoneIdentifier = '';
 
   protected $consoleEnabled = 0;
   protected $consoleVisible = 0;
   protected $consoleTab = '';
 
   protected $conduitCertificate;
 
   protected $isSystemAgent = 0;
   protected $isMailingList = 0;
   protected $isAdmin = 0;
   protected $isDisabled = 0;
   protected $isEmailVerified = 0;
   protected $isApproved = 0;
   protected $isEnrolledInMultiFactor = 0;
 
   protected $accountSecret;
 
   private $profileImage = self::ATTACHABLE;
   private $profile = null;
   private $availability = self::ATTACHABLE;
   private $preferences = null;
   private $omnipotent = false;
   private $customFields = self::ATTACHABLE;
 
   private $alternateCSRFString = self::ATTACHABLE;
   private $session = self::ATTACHABLE;
 
   private $authorities = array();
   private $handlePool;
   private $csrfSalt;
 
   protected function readField($field) {
     switch ($field) {
       case 'timezoneIdentifier':
         // If the user hasn't set one, guess the server's time.
         return nonempty(
           $this->timezoneIdentifier,
           date_default_timezone_get());
       // Make sure these return booleans.
       case 'isAdmin':
         return (bool)$this->isAdmin;
       case 'isDisabled':
         return (bool)$this->isDisabled;
       case 'isSystemAgent':
         return (bool)$this->isSystemAgent;
       case 'isMailingList':
         return (bool)$this->isMailingList;
       case 'isEmailVerified':
         return (bool)$this->isEmailVerified;
       case 'isApproved':
         return (bool)$this->isApproved;
       default:
         return parent::readField($field);
     }
   }
 
 
   /**
    * Is this a live account which has passed required approvals? Returns true
    * if this is an enabled, verified (if required), approved (if required)
    * account, and false otherwise.
    *
    * @return bool True if this is a standard, usable account.
    */
   public function isUserActivated() {
     if ($this->isOmnipotent()) {
       return true;
     }
 
     if ($this->getIsDisabled()) {
       return false;
     }
 
     if (!$this->getIsApproved()) {
       return false;
     }
 
     if (PhabricatorUserEmail::isEmailVerificationRequired()) {
       if (!$this->getIsEmailVerified()) {
         return false;
       }
     }
 
     return true;
   }
 
   public function canEstablishWebSessions() {
-    if (!$this->isUserActivated()) {
-      return false;
-    }
-
     if ($this->getIsMailingList()) {
       return false;
     }
 
     if ($this->getIsSystemAgent()) {
       return false;
     }
 
     return true;
   }
 
   public function canEstablishAPISessions() {
     if (!$this->isUserActivated()) {
       return false;
     }
 
     if ($this->getIsMailingList()) {
       return false;
     }
 
     return true;
   }
 
   public function canEstablishSSHSessions() {
     if (!$this->isUserActivated()) {
       return false;
     }
 
     if ($this->getIsMailingList()) {
       return false;
     }
 
     return true;
   }
 
   /**
    * Returns `true` if this is a standard user who is logged in. Returns `false`
    * for logged out, anonymous, or external users.
    *
    * @return bool `true` if the user is a standard user who is logged in with
    *              a normal session.
    */
   public function getIsStandardUser() {
     $type_user = PhabricatorPeopleUserPHIDType::TYPECONST;
     return $this->getPHID() && (phid_get_type($this->getPHID()) == $type_user);
   }
 
   protected function getConfiguration() {
     return array(
       self::CONFIG_AUX_PHID => true,
       self::CONFIG_COLUMN_SCHEMA => array(
         'userName' => 'sort64',
         'realName' => 'text128',
         'sex' => 'text4?',
         'translation' => 'text64?',
         'passwordSalt' => 'text32?',
         'passwordHash' => 'text128?',
         'profileImagePHID' => 'phid?',
         'consoleEnabled' => 'bool',
         'consoleVisible' => 'bool',
         'consoleTab' => 'text64',
         'conduitCertificate' => 'text255',
         'isSystemAgent' => 'bool',
         'isMailingList' => 'bool',
         'isDisabled' => 'bool',
         'isAdmin' => 'bool',
         'timezoneIdentifier' => 'text255',
         'isEmailVerified' => 'uint32',
         'isApproved' => 'uint32',
         'accountSecret' => 'bytes64',
         'isEnrolledInMultiFactor' => 'bool',
         'profileImageCache' => 'text255?',
         'availabilityCache' => 'text255?',
         'availabilityCacheTTL' => 'uint32?',
       ),
       self::CONFIG_KEY_SCHEMA => array(
         'key_phid' => null,
         'phid' => array(
           'columns' => array('phid'),
           'unique' => true,
         ),
         'userName' => array(
           'columns' => array('userName'),
           'unique' => true,
         ),
         'realName' => array(
           'columns' => array('realName'),
         ),
         'key_approved' => array(
           'columns' => array('isApproved'),
         ),
       ),
       self::CONFIG_NO_MUTATE => array(
         'profileImageCache' => true,
         'availabilityCache' => true,
         'availabilityCacheTTL' => true,
       ),
     ) + parent::getConfiguration();
   }
 
   public function generatePHID() {
     return PhabricatorPHID::generateNewPHID(
       PhabricatorPeopleUserPHIDType::TYPECONST);
   }
 
   public function setPassword(PhutilOpaqueEnvelope $envelope) {
     if (!$this->getPHID()) {
       throw new Exception(
         pht(
           'You can not set a password for an unsaved user because their PHID '.
           'is a salt component in the password hash.'));
     }
 
     if (!strlen($envelope->openEnvelope())) {
       $this->setPasswordHash('');
     } else {
       $this->setPasswordSalt(md5(Filesystem::readRandomBytes(32)));
       $hash = $this->hashPassword($envelope);
       $this->setPasswordHash($hash->openEnvelope());
     }
     return $this;
   }
 
   // To satisfy PhutilPerson.
   public function getSex() {
     return $this->sex;
   }
 
   public function getMonogram() {
     return '@'.$this->getUsername();
   }
 
   public function isLoggedIn() {
     return !($this->getPHID() === null);
   }
 
   public function save() {
     if (!$this->getConduitCertificate()) {
       $this->setConduitCertificate($this->generateConduitCertificate());
     }
 
     if (!strlen($this->getAccountSecret())) {
       $this->setAccountSecret(Filesystem::readRandomCharacters(64));
     }
 
     $result = parent::save();
 
     if ($this->profile) {
       $this->profile->save();
     }
 
     $this->updateNameTokens();
 
     id(new PhabricatorSearchIndexer())
       ->queueDocumentForIndexing($this->getPHID());
 
     return $result;
   }
 
   public function attachSession(PhabricatorAuthSession $session) {
     $this->session = $session;
     return $this;
   }
 
   public function getSession() {
     return $this->assertAttached($this->session);
   }
 
   public function hasSession() {
     return ($this->session !== self::ATTACHABLE);
   }
 
   private function generateConduitCertificate() {
     return Filesystem::readRandomCharacters(255);
   }
 
   public function comparePassword(PhutilOpaqueEnvelope $envelope) {
     if (!strlen($envelope->openEnvelope())) {
       return false;
     }
     if (!strlen($this->getPasswordHash())) {
       return false;
     }
 
     return PhabricatorPasswordHasher::comparePassword(
       $this->getPasswordHashInput($envelope),
       new PhutilOpaqueEnvelope($this->getPasswordHash()));
   }
 
   private function getPasswordHashInput(PhutilOpaqueEnvelope $password) {
     $input =
       $this->getUsername().
       $password->openEnvelope().
       $this->getPHID().
       $this->getPasswordSalt();
 
     return new PhutilOpaqueEnvelope($input);
   }
 
   private function hashPassword(PhutilOpaqueEnvelope $password) {
     $hasher = PhabricatorPasswordHasher::getBestHasher();
 
     $input_envelope = $this->getPasswordHashInput($password);
     return $hasher->getPasswordHashForStorage($input_envelope);
   }
 
   const CSRF_CYCLE_FREQUENCY  = 3600;
   const CSRF_SALT_LENGTH      = 8;
   const CSRF_TOKEN_LENGTH     = 16;
   const CSRF_BREACH_PREFIX    = 'B@';
 
   const EMAIL_CYCLE_FREQUENCY = 86400;
   const EMAIL_TOKEN_LENGTH    = 24;
 
   private function getRawCSRFToken($offset = 0) {
     return $this->generateToken(
       time() + (self::CSRF_CYCLE_FREQUENCY * $offset),
       self::CSRF_CYCLE_FREQUENCY,
       PhabricatorEnv::getEnvConfig('phabricator.csrf-key'),
       self::CSRF_TOKEN_LENGTH);
   }
 
   public function getCSRFToken() {
     if ($this->isOmnipotent()) {
       // We may end up here when called from the daemons. The omnipotent user
       // has no meaningful CSRF token, so just return `null`.
       return null;
     }
 
     if ($this->csrfSalt === null) {
       $this->csrfSalt = Filesystem::readRandomCharacters(
         self::CSRF_SALT_LENGTH);
     }
 
     $salt = $this->csrfSalt;
 
     // Generate a token hash to mitigate BREACH attacks against SSL. See
     // discussion in T3684.
     $token = $this->getRawCSRFToken();
     $hash = PhabricatorHash::digest($token, $salt);
     return self::CSRF_BREACH_PREFIX.$salt.substr(
         $hash, 0, self::CSRF_TOKEN_LENGTH);
   }
 
   public function validateCSRFToken($token) {
     $salt = null;
     $version = 'plain';
 
     // This is a BREACH-mitigating token. See T3684.
     $breach_prefix = self::CSRF_BREACH_PREFIX;
     $breach_prelen = strlen($breach_prefix);
 
     if (!strncmp($token, $breach_prefix, $breach_prelen)) {
       $version = 'breach';
       $salt = substr($token, $breach_prelen, self::CSRF_SALT_LENGTH);
       $token = substr($token, $breach_prelen + self::CSRF_SALT_LENGTH);
     }
 
     // When the user posts a form, we check that it contains a valid CSRF token.
     // Tokens cycle each hour (every CSRF_CYLCE_FREQUENCY seconds) and we accept
     // either the current token, the next token (users can submit a "future"
     // token if you have two web frontends that have some clock skew) or any of
     // the last 6 tokens. This means that pages are valid for up to 7 hours.
     // There is also some Javascript which periodically refreshes the CSRF
     // tokens on each page, so theoretically pages should be valid indefinitely.
     // However, this code may fail to run (if the user loses their internet
     // connection, or there's a JS problem, or they don't have JS enabled).
     // Choosing the size of the window in which we accept old CSRF tokens is
     // an issue of balancing concerns between security and usability. We could
     // choose a very narrow (e.g., 1-hour) window to reduce vulnerability to
     // attacks using captured CSRF tokens, but it's also more likely that real
     // users will be affected by this, e.g. if they close their laptop for an
     // hour, open it back up, and try to submit a form before the CSRF refresh
     // can kick in. Since the user experience of submitting a form with expired
     // CSRF is often quite bad (you basically lose data, or it's a big pain to
     // recover at least) and I believe we gain little additional protection
     // by keeping the window very short (the overwhelming value here is in
     // preventing blind attacks, and most attacks which can capture CSRF tokens
     // can also just capture authentication information [sniffing networks]
     // or act as the user [xss]) the 7 hour default seems like a reasonable
     // balance. Other major platforms have much longer CSRF token lifetimes,
     // like Rails (session duration) and Django (forever), which suggests this
     // is a reasonable analysis.
     $csrf_window = 6;
 
     for ($ii = -$csrf_window; $ii <= 1; $ii++) {
       $valid = $this->getRawCSRFToken($ii);
       switch ($version) {
         // TODO: We can remove this after the BREACH version has been in the
         // wild for a while.
         case 'plain':
           if ($token == $valid) {
             return true;
           }
           break;
         case 'breach':
           $digest = PhabricatorHash::digest($valid, $salt);
           if (substr($digest, 0, self::CSRF_TOKEN_LENGTH) == $token) {
             return true;
           }
           break;
         default:
           throw new Exception(pht('Unknown CSRF token format!'));
       }
     }
 
     return false;
   }
 
   private function generateToken($epoch, $frequency, $key, $len) {
     if ($this->getPHID()) {
       $vec = $this->getPHID().$this->getAccountSecret();
     } else {
       $vec = $this->getAlternateCSRFString();
     }
 
     if ($this->hasSession()) {
       $vec = $vec.$this->getSession()->getSessionKey();
     }
 
     $time_block = floor($epoch / $frequency);
     $vec = $vec.$key.$time_block;
 
     return substr(PhabricatorHash::digest($vec), 0, $len);
   }
 
   public function getUserProfile() {
     return $this->assertAttached($this->profile);
   }
 
   public function attachUserProfile(PhabricatorUserProfile $profile) {
     $this->profile = $profile;
     return $this;
   }
 
   public function loadUserProfile() {
     if ($this->profile) {
       return $this->profile;
     }
 
     $profile_dao = new PhabricatorUserProfile();
     $this->profile = $profile_dao->loadOneWhere('userPHID = %s',
       $this->getPHID());
 
     if (!$this->profile) {
       $profile_dao->setUserPHID($this->getPHID());
       $this->profile = $profile_dao;
     }
 
     return $this->profile;
   }
 
   public function loadPrimaryEmailAddress() {
     $email = $this->loadPrimaryEmail();
     if (!$email) {
       throw new Exception(pht('User has no primary email address!'));
     }
     return $email->getAddress();
   }
 
   public function loadPrimaryEmail() {
     return $this->loadOneRelative(
       new PhabricatorUserEmail(),
       'userPHID',
       'getPHID',
       '(isPrimary = 1)');
   }
 
   public function loadPreferences() {
     if ($this->preferences) {
       return $this->preferences;
     }
 
     $preferences = null;
     if ($this->getPHID()) {
       $preferences = id(new PhabricatorUserPreferences())->loadOneWhere(
         'userPHID = %s',
         $this->getPHID());
     }
 
     if (!$preferences) {
       $preferences = new PhabricatorUserPreferences();
       $preferences->setUserPHID($this->getPHID());
 
       $default_dict = array(
         PhabricatorUserPreferences::PREFERENCE_TITLES => 'glyph',
         PhabricatorUserPreferences::PREFERENCE_EDITOR => '',
         PhabricatorUserPreferences::PREFERENCE_MONOSPACED => '',
         PhabricatorUserPreferences::PREFERENCE_DARK_CONSOLE => 0,
       );
 
       $preferences->setPreferences($default_dict);
     }
 
     $this->preferences = $preferences;
     return $preferences;
   }
 
   public function loadEditorLink($path, $line, $callsign) {
     $editor = $this->loadPreferences()->getPreference(
       PhabricatorUserPreferences::PREFERENCE_EDITOR);
 
     if (is_array($path)) {
       $multiedit = $this->loadPreferences()->getPreference(
         PhabricatorUserPreferences::PREFERENCE_MULTIEDIT);
       switch ($multiedit) {
         case '':
           $path = implode(' ', $path);
           break;
         case 'disable':
           return null;
       }
     }
 
     if (!strlen($editor)) {
       return null;
     }
 
     $uri = strtr($editor, array(
       '%%' => '%',
       '%f' => phutil_escape_uri($path),
       '%l' => phutil_escape_uri($line),
       '%r' => phutil_escape_uri($callsign),
     ));
 
     // The resulting URI must have an allowed protocol. Otherwise, we'll return
     // a link to an error page explaining the misconfiguration.
 
     $ok = PhabricatorHelpEditorProtocolController::hasAllowedProtocol($uri);
     if (!$ok) {
       return '/help/editorprotocol/';
     }
 
     return (string)$uri;
   }
 
   public function getAlternateCSRFString() {
     return $this->assertAttached($this->alternateCSRFString);
   }
 
   public function attachAlternateCSRFString($string) {
     $this->alternateCSRFString = $string;
     return $this;
   }
 
   /**
    * Populate the nametoken table, which used to fetch typeahead results. When
    * a user types "linc", we want to match "Abraham Lincoln" from on-demand
    * typeahead sources. To do this, we need a separate table of name fragments.
    */
   public function updateNameTokens() {
     $table  = self::NAMETOKEN_TABLE;
     $conn_w = $this->establishConnection('w');
 
     $tokens = PhabricatorTypeaheadDatasource::tokenizeString(
       $this->getUserName().' '.$this->getRealName());
 
     $sql = array();
     foreach ($tokens as $token) {
       $sql[] = qsprintf(
         $conn_w,
         '(%d, %s)',
         $this->getID(),
         $token);
     }
 
     queryfx(
       $conn_w,
       'DELETE FROM %T WHERE userID = %d',
       $table,
       $this->getID());
     if ($sql) {
       queryfx(
         $conn_w,
         'INSERT INTO %T (userID, token) VALUES %Q',
         $table,
         implode(', ', $sql));
     }
   }
 
   public function sendWelcomeEmail(PhabricatorUser $admin) {
     $admin_username = $admin->getUserName();
     $admin_realname = $admin->getRealName();
     $user_username = $this->getUserName();
     $is_serious = PhabricatorEnv::getEnvConfig('phabricator.serious-business');
 
     $base_uri = PhabricatorEnv::getProductionURI('/');
 
     $engine = new PhabricatorAuthSessionEngine();
     $uri = $engine->getOneTimeLoginURI(
       $this,
       $this->loadPrimaryEmail(),
       PhabricatorAuthSessionEngine::ONETIME_WELCOME);
 
     $body = pht(
       "Welcome to Phabricator!\n\n".
       "%s (%s) has created an account for you.\n\n".
       "  Username: %s\n\n".
       "To login to Phabricator, follow this link and set a password:\n\n".
       "  %s\n\n".
       "After you have set a password, you can login in the future by ".
       "going here:\n\n".
       "  %s\n",
       $admin_username,
       $admin_realname,
       $user_username,
       $uri,
       $base_uri);
 
     if (!$is_serious) {
       $body .= sprintf(
         "\n%s\n",
         pht("Love,\nPhabricator"));
     }
 
     $mail = id(new PhabricatorMetaMTAMail())
       ->addTos(array($this->getPHID()))
       ->setForceDelivery(true)
       ->setSubject(pht('[Phabricator] Welcome to Phabricator'))
       ->setBody($body)
       ->saveAndSend();
   }
 
   public function sendUsernameChangeEmail(
     PhabricatorUser $admin,
     $old_username) {
 
     $admin_username = $admin->getUserName();
     $admin_realname = $admin->getRealName();
     $new_username = $this->getUserName();
 
     $password_instructions = null;
     if (PhabricatorPasswordAuthProvider::getPasswordProvider()) {
       $engine = new PhabricatorAuthSessionEngine();
       $uri = $engine->getOneTimeLoginURI(
         $this,
         null,
         PhabricatorAuthSessionEngine::ONETIME_USERNAME);
       $password_instructions = sprintf(
         "%s\n\n  %s\n\n%s\n",
         pht(
           "If you use a password to login, you'll need to reset it ".
           "before you can login again. You can reset your password by ".
           "following this link:"),
         $uri,
         pht(
           "And, of course, you'll need to use your new username to login ".
           "from now on. If you use OAuth to login, nothing should change."));
     }
 
     $body = sprintf(
       "%s\n\n  %s\n  %s\n\n%s",
       pht(
         '%s (%s) has changed your Phabricator username.',
         $admin_username,
         $admin_realname),
       pht(
         'Old Username: %s',
         $old_username),
       pht(
         'New Username: %s',
         $new_username),
       $password_instructions);
 
     $mail = id(new PhabricatorMetaMTAMail())
       ->addTos(array($this->getPHID()))
       ->setForceDelivery(true)
       ->setSubject(pht('[Phabricator] Username Changed'))
       ->setBody($body)
       ->saveAndSend();
   }
 
   public static function describeValidUsername() {
     return pht(
       'Usernames must contain only numbers, letters, period, underscore and '.
       'hyphen, and can not end with a period. They must have no more than %d '.
       'characters.',
       new PhutilNumber(self::MAXIMUM_USERNAME_LENGTH));
   }
 
   public static function validateUsername($username) {
     // NOTE: If you update this, make sure to update:
     //
     //  - Remarkup rule for @mentions.
     //  - Routing rule for "/p/username/".
     //  - Unit tests, obviously.
     //  - describeValidUsername() method, above.
 
     if (strlen($username) > self::MAXIMUM_USERNAME_LENGTH) {
       return false;
     }
 
     return (bool)preg_match('/^[a-zA-Z0-9._-]*[a-zA-Z0-9_-]\z/', $username);
   }
 
   public static function getDefaultProfileImageURI() {
     return celerity_get_resource_uri('/rsrc/image/avatar.png');
   }
 
   public function attachProfileImageURI($uri) {
     $this->profileImage = $uri;
     return $this;
   }
 
   public function getProfileImageURI() {
     return $this->assertAttached($this->profileImage);
   }
 
   public function getFullName() {
     if (strlen($this->getRealName())) {
       return $this->getUsername().' ('.$this->getRealName().')';
     } else {
       return $this->getUsername();
     }
   }
 
   public function getTimeZone() {
     return new DateTimeZone($this->getTimezoneIdentifier());
   }
 
   public function __toString() {
     return $this->getUsername();
   }
 
   public static function loadOneWithEmailAddress($address) {
     $email = id(new PhabricatorUserEmail())->loadOneWhere(
       'address = %s',
       $address);
     if (!$email) {
       return null;
     }
     return id(new PhabricatorUser())->loadOneWhere(
       'phid = %s',
       $email->getUserPHID());
   }
 
   public function getDefaultSpacePHID() {
     // TODO: We might let the user switch which space they're "in" later on;
     // for now just use the global space if one exists.
 
-    $spaces = PhabricatorSpacesNamespaceQuery::getViewerSpaces($this);
+    // If the viewer has access to the default space, use that.
+    $spaces = PhabricatorSpacesNamespaceQuery::getViewerActiveSpaces($this);
     foreach ($spaces as $space) {
       if ($space->getIsDefaultNamespace()) {
         return $space->getPHID();
       }
     }
 
+    // Otherwise, use the space with the lowest ID that they have access to.
+    // This just tends to keep the default stable and predictable over time,
+    // so adding a new space won't change behavior for users.
+    if ($spaces) {
+      $spaces = msort($spaces, 'getID');
+      return head($spaces)->getPHID();
+    }
+
     return null;
   }
 
 
   /**
    * Grant a user a source of authority, to let them bypass policy checks they
    * could not otherwise.
    */
   public function grantAuthority($authority) {
     $this->authorities[] = $authority;
     return $this;
   }
 
 
   /**
    * Get authorities granted to the user.
    */
   public function getAuthorities() {
     return $this->authorities;
   }
 
 
 /* -(  Availability  )------------------------------------------------------- */
 
 
   /**
    * @task availability
    */
   public function attachAvailability(array $availability) {
     $this->availability = $availability;
     return $this;
   }
 
 
   /**
    * Get the timestamp the user is away until, if they are currently away.
    *
    * @return int|null Epoch timestamp, or `null` if the user is not away.
    * @task availability
    */
   public function getAwayUntil() {
     $availability = $this->availability;
 
     $this->assertAttached($availability);
     if (!$availability) {
       return null;
     }
 
     return idx($availability, 'until');
   }
 
 
   /**
    * Describe the user's availability.
    *
    * @param PhabricatorUser Viewing user.
    * @return string Human-readable description of away status.
    * @task availability
    */
   public function getAvailabilityDescription(PhabricatorUser $viewer) {
     $until = $this->getAwayUntil();
     if ($until) {
       return pht('Away until %s', phabricator_datetime($until, $viewer));
     } else {
       return pht('Available');
     }
   }
 
 
   /**
    * Get cached availability, if present.
    *
    * @return wild|null Cache data, or null if no cache is available.
    * @task availability
    */
   public function getAvailabilityCache() {
     $now = PhabricatorTime::getNow();
     if ($this->availabilityCacheTTL <= $now) {
       return null;
     }
 
     try {
       return phutil_json_decode($this->availabilityCache);
     } catch (Exception $ex) {
       return null;
     }
   }
 
 
   /**
    * Write to the availability cache.
    *
    * @param wild Availability cache data.
    * @param int|null Cache TTL.
    * @return this
    * @task availability
    */
   public function writeAvailabilityCache(array $availability, $ttl) {
     $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
     queryfx(
       $this->establishConnection('w'),
       'UPDATE %T SET availabilityCache = %s, availabilityCacheTTL = %nd
         WHERE id = %d',
       $this->getTableName(),
       json_encode($availability),
       $ttl,
       $this->getID());
     unset($unguarded);
 
     return $this;
   }
 
 
 /* -(  Profile Image Cache  )------------------------------------------------ */
 
 
   /**
    * Get this user's cached profile image URI.
    *
    * @return string|null Cached URI, if a URI is cached.
    * @task image-cache
    */
   public function getProfileImageCache() {
     $version = $this->getProfileImageVersion();
 
     $parts = explode(',', $this->profileImageCache, 2);
     if (count($parts) !== 2) {
       return null;
     }
 
     if ($parts[0] !== $version) {
       return null;
     }
 
     return $parts[1];
   }
 
 
   /**
    * Generate a new cache value for this user's profile image.
    *
    * @return string New cache value.
    * @task image-cache
    */
   public function writeProfileImageCache($uri) {
     $version = $this->getProfileImageVersion();
     $cache = "{$version},{$uri}";
 
     $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
     queryfx(
       $this->establishConnection('w'),
       'UPDATE %T SET profileImageCache = %s WHERE id = %d',
       $this->getTableName(),
       $cache,
       $this->getID());
     unset($unguarded);
   }
 
 
   /**
    * Get a version identifier for a user's profile image.
    *
    * This version will change if the image changes, or if any of the
    * environment configuration which goes into generating a URI changes.
    *
    * @return string Cache version.
    * @task image-cache
    */
   private function getProfileImageVersion() {
     $parts = array(
       PhabricatorEnv::getCDNURI('/'),
       PhabricatorEnv::getEnvConfig('cluster.instance'),
       $this->getProfileImagePHID(),
     );
     $parts = serialize($parts);
     return PhabricatorHash::digestForIndex($parts);
   }
 
 
 /* -(  Multi-Factor Authentication  )---------------------------------------- */
 
 
   /**
    * Update the flag storing this user's enrollment in multi-factor auth.
    *
    * With certain settings, we need to check if a user has MFA on every page,
    * so we cache MFA enrollment on the user object for performance. Calling this
    * method synchronizes the cache by examining enrollment records. After
    * updating the cache, use @{method:getIsEnrolledInMultiFactor} to check if
    * the user is enrolled.
    *
    * This method should be called after any changes are made to a given user's
    * multi-factor configuration.
    *
    * @return void
    * @task factors
    */
   public function updateMultiFactorEnrollment() {
     $factors = id(new PhabricatorAuthFactorConfig())->loadAllWhere(
       'userPHID = %s',
       $this->getPHID());
 
     $enrolled = count($factors) ? 1 : 0;
     if ($enrolled !== $this->isEnrolledInMultiFactor) {
       $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
         queryfx(
           $this->establishConnection('w'),
           'UPDATE %T SET isEnrolledInMultiFactor = %d WHERE id = %d',
           $this->getTableName(),
           $enrolled,
           $this->getID());
       unset($unguarded);
 
       $this->isEnrolledInMultiFactor = $enrolled;
     }
   }
 
 
   /**
    * Check if the user is enrolled in multi-factor authentication.
    *
    * Enrolled users have one or more multi-factor authentication sources
    * attached to their account. For performance, this value is cached. You
    * can use @{method:updateMultiFactorEnrollment} to update the cache.
    *
    * @return bool True if the user is enrolled.
    * @task factors
    */
   public function getIsEnrolledInMultiFactor() {
     return $this->isEnrolledInMultiFactor;
   }
 
 
 /* -(  Omnipotence  )-------------------------------------------------------- */
 
 
   /**
    * Returns true if this user is omnipotent. Omnipotent users bypass all policy
    * checks.
    *
    * @return bool True if the user bypasses policy checks.
    */
   public function isOmnipotent() {
     return $this->omnipotent;
   }
 
 
   /**
    * Get an omnipotent user object for use in contexts where there is no acting
    * user, notably daemons.
    *
    * @return PhabricatorUser An omnipotent user.
    */
   public static function getOmnipotentUser() {
     static $user = null;
     if (!$user) {
       $user = new PhabricatorUser();
       $user->omnipotent = true;
       $user->makeEphemeral();
     }
     return $user;
   }
 
 
 /* -(  Managing Handles  )--------------------------------------------------- */
 
 
   /**
    * Get a @{class:PhabricatorHandleList} which benefits from this viewer's
    * internal handle pool.
    *
    * @param list<phid> List of PHIDs to load.
    * @return PhabricatorHandleList Handle list object.
    * @task handle
    */
   public function loadHandles(array $phids) {
     if ($this->handlePool === null) {
       $this->handlePool = id(new PhabricatorHandlePool())
         ->setViewer($this);
     }
 
     return $this->handlePool->newHandleList($phids);
   }
 
 
   /**
    * Get a @{class:PHUIHandleView} for a single handle.
    *
    * This benefits from the viewer's internal handle pool.
    *
    * @param phid PHID to render a handle for.
    * @return PHUIHandleView View of the handle.
    * @task handle
    */
   public function renderHandle($phid) {
     return $this->loadHandles(array($phid))->renderHandle($phid);
   }
 
 
   /**
    * Get a @{class:PHUIHandleListView} for a list of handles.
    *
    * This benefits from the viewer's internal handle pool.
    *
    * @param list<phid> List of PHIDs to render.
    * @return PHUIHandleListView View of the handles.
    * @task handle
    */
   public function renderHandleList(array $phids) {
     return $this->loadHandles($phids)->renderList();
   }
 
 
 /* -(  PhabricatorPolicyInterface  )----------------------------------------- */
 
 
   public function getCapabilities() {
     return array(
       PhabricatorPolicyCapability::CAN_VIEW,
       PhabricatorPolicyCapability::CAN_EDIT,
     );
   }
 
   public function getPolicy($capability) {
     switch ($capability) {
       case PhabricatorPolicyCapability::CAN_VIEW:
         return PhabricatorPolicies::POLICY_PUBLIC;
       case PhabricatorPolicyCapability::CAN_EDIT:
         if ($this->getIsSystemAgent() || $this->getIsMailingList()) {
           return PhabricatorPolicies::POLICY_ADMIN;
         } else {
           return PhabricatorPolicies::POLICY_NOONE;
         }
     }
   }
 
   public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
     return $this->getPHID() && ($viewer->getPHID() === $this->getPHID());
   }
 
   public function describeAutomaticCapability($capability) {
     switch ($capability) {
       case PhabricatorPolicyCapability::CAN_EDIT:
         return pht('Only you can edit your information.');
       default:
         return null;
     }
   }
 
 
 /* -(  PhabricatorCustomFieldInterface  )------------------------------------ */
 
 
   public function getCustomFieldSpecificationForRole($role) {
     return PhabricatorEnv::getEnvConfig('user.fields');
   }
 
   public function getCustomFieldBaseClass() {
     return 'PhabricatorUserCustomField';
   }
 
   public function getCustomFields() {
     return $this->assertAttached($this->customFields);
   }
 
   public function attachCustomFields(PhabricatorCustomFieldAttachment $fields) {
     $this->customFields = $fields;
     return $this;
   }
 
 
 /* -(  PhabricatorDestructibleInterface  )----------------------------------- */
 
 
   public function destroyObjectPermanently(
     PhabricatorDestructionEngine $engine) {
 
     $this->openTransaction();
       $this->delete();
 
       $externals = id(new PhabricatorExternalAccount())->loadAllWhere(
         'userPHID = %s',
         $this->getPHID());
       foreach ($externals as $external) {
         $external->delete();
       }
 
       $prefs = id(new PhabricatorUserPreferences())->loadAllWhere(
         'userPHID = %s',
         $this->getPHID());
       foreach ($prefs as $pref) {
         $pref->delete();
       }
 
       $profiles = id(new PhabricatorUserProfile())->loadAllWhere(
         'userPHID = %s',
         $this->getPHID());
       foreach ($profiles as $profile) {
         $profile->delete();
       }
 
       $keys = id(new PhabricatorAuthSSHKey())->loadAllWhere(
         'objectPHID = %s',
         $this->getPHID());
       foreach ($keys as $key) {
         $key->delete();
       }
 
       $emails = id(new PhabricatorUserEmail())->loadAllWhere(
         'userPHID = %s',
         $this->getPHID());
       foreach ($emails as $email) {
         $email->delete();
       }
 
       $sessions = id(new PhabricatorAuthSession())->loadAllWhere(
         'userPHID = %s',
         $this->getPHID());
       foreach ($sessions as $session) {
         $session->delete();
       }
 
       $factors = id(new PhabricatorAuthFactorConfig())->loadAllWhere(
         'userPHID = %s',
         $this->getPHID());
       foreach ($factors as $factor) {
         $factor->delete();
       }
 
     $this->saveTransaction();
   }
 
 
 /* -(  PhabricatorSSHPublicKeyInterface  )----------------------------------- */
 
 
   public function getSSHPublicKeyManagementURI(PhabricatorUser $viewer) {
     if ($viewer->getPHID() == $this->getPHID()) {
       // If the viewer is managing their own keys, take them to the normal
       // panel.
       return '/settings/panel/ssh/';
     } else {
       // Otherwise, take them to the administrative panel for this user.
       return '/settings/'.$this->getID().'/panel/ssh/';
     }
   }
 
   public function getSSHKeyDefaultName() {
     return 'id_rsa_phabricator';
   }
 
 
 /* -(  PhabricatorApplicationTransactionInterface  )------------------------- */
 
 
   public function getApplicationTransactionEditor() {
     return new PhabricatorUserProfileEditor();
   }
 
   public function getApplicationTransactionObject() {
     return $this;
   }
 
   public function getApplicationTransactionTemplate() {
     return new PhabricatorUserTransaction();
   }
 
   public function willRenderTimeline(
     PhabricatorApplicationTransactionView $timeline,
     AphrontRequest $request) {
     return $timeline;
   }
 
 }
diff --git a/src/applications/phame/skins/PhameSkinSpecification.php b/src/applications/phame/skins/PhameSkinSpecification.php
index 7dedf8ab4..9e0265392 100644
--- a/src/applications/phame/skins/PhameSkinSpecification.php
+++ b/src/applications/phame/skins/PhameSkinSpecification.php
@@ -1,197 +1,197 @@
 <?php
 
-final class PhameSkinSpecification {
+final class PhameSkinSpecification extends Phobject {
 
   const TYPE_ADVANCED   = 'advanced';
   const TYPE_BASIC      = 'basic';
 
   private $type;
   private $rootDirectory;
   private $skinClass;
   private $phutilLibraries = array();
   private $name;
   private $config;
 
   public static function loadAllSkinSpecifications() {
     static $specs;
 
     if ($specs === null) {
       $paths = PhabricatorEnv::getEnvConfig('phame.skins');
       $base  = dirname(phutil_get_library_root('phabricator'));
 
       $specs = array();
 
       foreach ($paths as $path) {
         $path = Filesystem::resolvePath($path, $base);
         foreach (Filesystem::listDirectory($path) as $skin_directory) {
           $skin_path = $path.DIRECTORY_SEPARATOR.$skin_directory;
 
           if (!is_dir($skin_path)) {
             continue;
           }
           $spec = self::loadSkinSpecification($skin_path);
           if (!$spec) {
             continue;
           }
 
           $name = trim($skin_directory, DIRECTORY_SEPARATOR);
 
           $spec->setName($name);
 
           if (isset($specs[$name])) {
             $that_dir = $specs[$name]->getRootDirectory();
             $this_dir = $spec->getRootDirectory();
             throw new Exception(
               pht(
                 "Two skins have the same name ('%s'), in '%s' and '%s'. ".
                 "Rename one or adjust your '%s' configuration.",
                 $name,
                 $this_dir,
                 $that_dir,
                 'phame.skins'));
           }
 
           $specs[$name] = $spec;
         }
       }
     }
 
     return $specs;
   }
 
   public static function loadOneSkinSpecification($name) {
     // Only allow skins which we know to exist to load. This prevents loading
     // skins like "../../secrets/evil/".
     $all = self::loadAllSkinSpecifications();
     if (empty($all[$name])) {
       throw new Exception(
         pht(
           'Blog skin "%s" is not a valid skin!',
           $name));
     }
 
     $paths = PhabricatorEnv::getEnvConfig('phame.skins');
     $base = dirname(phutil_get_library_root('phabricator'));
     foreach ($paths as $path) {
       $path = Filesystem::resolvePath($path, $base);
       $skin_path = $path.DIRECTORY_SEPARATOR.$name;
       if (is_dir($skin_path)) {
 
         // Double check that the skin really lives in the skin directory.
         if (!Filesystem::isDescendant($skin_path, $path)) {
           throw new Exception(
             pht(
               'Blog skin "%s" is not located in path "%s"!',
               $name,
               $path));
         }
 
         $spec = self::loadSkinSpecification($skin_path);
         if ($spec) {
           $spec->setName($name);
           return $spec;
         }
       }
     }
     return null;
   }
 
   private static function loadSkinSpecification($path) {
     $config_path = $path.DIRECTORY_SEPARATOR.'skin.json';
     $config = array();
     if (Filesystem::pathExists($config_path)) {
       $config = Filesystem::readFile($config_path);
       try {
         $config = phutil_json_decode($config);
       } catch (PhutilJSONParserException $ex) {
         throw new PhutilProxyException(
           pht(
             "Skin configuration file '%s' is not a valid JSON file.",
             $config_path),
           $ex);
       }
       $type = idx($config, 'type', self::TYPE_BASIC);
     } else {
       $type = self::TYPE_BASIC;
     }
 
     $spec = new PhameSkinSpecification();
     $spec->setRootDirectory($path);
     $spec->setConfig($config);
 
     switch ($type) {
       case self::TYPE_BASIC:
         $spec->setSkinClass('PhameBasicTemplateBlogSkin');
         break;
       case self::TYPE_ADVANCED:
         $spec->setSkinClass($config['class']);
         $spec->addPhutilLibrary($path.DIRECTORY_SEPARATOR.'src');
         break;
       default:
         throw new Exception(pht('Unknown skin type!'));
     }
 
     $spec->setType($type);
 
     return $spec;
   }
 
   public function setConfig(array $config) {
     $this->config = $config;
     return $this;
   }
 
   public function getConfig($key, $default = null) {
     return idx($this->config, $key, $default);
   }
 
   public function setName($name) {
     $this->name = $name;
     return $this;
   }
 
   public function getName() {
     return $this->getConfig('name', $this->name);
   }
 
   public function setRootDirectory($root_directory) {
     $this->rootDirectory = $root_directory;
     return $this;
   }
 
   public function getRootDirectory() {
     return $this->rootDirectory;
   }
 
   public function setType($type) {
     $this->type = $type;
     return $this;
   }
 
   public function getType() {
     return $this->type;
   }
 
   public function setSkinClass($skin_class) {
     $this->skinClass = $skin_class;
     return $this;
   }
 
   public function getSkinClass() {
     return $this->skinClass;
   }
 
   public function addPhutilLibrary($library) {
     $this->phutilLibraries[] = $library;
     return $this;
   }
 
   public function buildSkin(AphrontRequest $request) {
     foreach ($this->phutilLibraries as $library) {
       phutil_load_library($library);
     }
 
     return newv($this->getSkinClass(), array($request, $this));
   }
 
 }
diff --git a/src/applications/phid/PhabricatorObjectHandle.php b/src/applications/phid/PhabricatorObjectHandle.php
index 2f7cda837..283ad0fb6 100644
--- a/src/applications/phid/PhabricatorObjectHandle.php
+++ b/src/applications/phid/PhabricatorObjectHandle.php
@@ -1,337 +1,338 @@
 <?php
 
 final class PhabricatorObjectHandle
+  extends Phobject
   implements PhabricatorPolicyInterface {
 
   const AVAILABILITY_FULL = 'full';
   const AVAILABILITY_NONE = 'none';
   const AVAILABILITY_PARTIAL = 'partial';
   const AVAILABILITY_DISABLED = 'disabled';
 
   const STATUS_OPEN = 'open';
   const STATUS_CLOSED = 'closed';
 
   private $uri;
   private $phid;
   private $type;
   private $name;
   private $fullName;
   private $title;
   private $imageURI;
   private $icon;
   private $tagColor;
   private $timestamp;
   private $status = self::STATUS_OPEN;
   private $availability = self::AVAILABILITY_FULL;
   private $complete;
   private $objectName;
   private $policyFiltered;
 
   public function setIcon($icon) {
     $this->icon = $icon;
     return $this;
   }
 
   public function getIcon() {
     if ($this->getPolicyFiltered()) {
       return 'fa-lock';
     }
 
     if ($this->icon) {
       return $this->icon;
     }
     return $this->getTypeIcon();
   }
 
   public function setTagColor($color) {
     static $colors;
     if (!$colors) {
       $colors = array_fuse(array_keys(PHUITagView::getShadeMap()));
     }
 
     if (isset($colors[$color])) {
       $this->tagColor = $color;
     }
 
     return $this;
   }
 
   public function getTagColor() {
     if ($this->getPolicyFiltered()) {
       return 'disabled';
     }
 
     if ($this->tagColor) {
       return $this->tagColor;
     }
 
     return 'blue';
   }
 
   public function getIconColor() {
     if ($this->tagColor) {
       return $this->tagColor;
     }
     return null;
   }
 
   public function getTypeIcon() {
     if ($this->getPHIDType()) {
       return $this->getPHIDType()->getTypeIcon();
     }
     return null;
   }
 
   public function setPolicyFiltered($policy_filered) {
     $this->policyFiltered = $policy_filered;
     return $this;
   }
 
   public function getPolicyFiltered() {
     return $this->policyFiltered;
   }
 
   public function setObjectName($object_name) {
     $this->objectName = $object_name;
     return $this;
   }
 
   public function getObjectName() {
     if (!$this->objectName) {
       return $this->getName();
     }
     return $this->objectName;
   }
 
   public function setURI($uri) {
     $this->uri = $uri;
     return $this;
   }
 
   public function getURI() {
     return $this->uri;
   }
 
   public function setPHID($phid) {
     $this->phid = $phid;
     return $this;
   }
 
   public function getPHID() {
     return $this->phid;
   }
 
   public function setName($name) {
     $this->name = $name;
     return $this;
   }
 
   public function getName() {
     if ($this->name === null) {
       if ($this->getPolicyFiltered()) {
         return pht('Restricted %s', $this->getTypeName());
       } else {
         return pht('Unknown Object (%s)', $this->getTypeName());
       }
     }
     return $this->name;
   }
 
   public function setAvailability($availability) {
     $this->availability = $availability;
     return $this;
   }
 
   public function getAvailability() {
     return $this->availability;
   }
 
   public function isDisabled() {
     return ($this->getAvailability() == self::AVAILABILITY_DISABLED);
   }
 
   public function setStatus($status) {
     $this->status = $status;
     return $this;
   }
 
   public function getStatus() {
     return $this->status;
   }
 
   public function setFullName($full_name) {
     $this->fullName = $full_name;
     return $this;
   }
 
   public function getFullName() {
     if ($this->fullName !== null) {
       return $this->fullName;
     }
     return $this->getName();
   }
 
   public function setTitle($title) {
     $this->title = $title;
     return $this;
   }
 
   public function getTitle() {
     return $this->title;
   }
 
   public function setType($type) {
     $this->type = $type;
     return $this;
   }
 
   public function getType() {
     return $this->type;
   }
 
   public function setImageURI($uri) {
     $this->imageURI = $uri;
     return $this;
   }
 
   public function getImageURI() {
     return $this->imageURI;
   }
 
   public function setTimestamp($timestamp) {
     $this->timestamp = $timestamp;
     return $this;
   }
 
   public function getTimestamp() {
     return $this->timestamp;
   }
 
   public function getTypeName() {
     if ($this->getPHIDType()) {
       return $this->getPHIDType()->getTypeName();
     }
 
     return $this->getType();
   }
 
 
   /**
    * Set whether or not the underlying object is complete. See
    * @{method:isComplete} for an explanation of what it means to be complete.
    *
    * @param bool True if the handle represents a complete object.
    * @return this
    */
   public function setComplete($complete) {
     $this->complete = $complete;
     return $this;
   }
 
 
   /**
    * Determine if the handle represents an object which was completely loaded
    * (i.e., the underlying object exists) vs an object which could not be
    * completely loaded (e.g., the type or data for the PHID could not be
    * identified or located).
    *
    * Basically, @{class:PhabricatorHandleQuery} gives you back a handle for
    * any PHID you give it, but it gives you a complete handle only for valid
    * PHIDs.
    *
    * @return bool True if the handle represents a complete object.
    */
   public function isComplete() {
     return $this->complete;
   }
 
 
   public function renderLink($name = null) {
     if ($name === null) {
       $name = $this->getLinkName();
     }
     $classes = array();
     $classes[] = 'phui-handle';
     $title = $this->title;
 
     if ($this->status != self::STATUS_OPEN) {
       $classes[] = 'handle-status-'.$this->status;
     }
 
     if ($this->availability != self::AVAILABILITY_FULL) {
       $classes[] = 'handle-availability-'.$this->availability;
     }
 
     if ($this->getType() == PhabricatorPeopleUserPHIDType::TYPECONST) {
       $classes[] = 'phui-link-person';
     }
 
     $uri = $this->getURI();
 
     $icon = null;
     if ($this->getPolicyFiltered()) {
       $icon = id(new PHUIIconView())
         ->setIconFont('fa-lock lightgreytext');
     }
 
     return phutil_tag(
       $uri ? 'a' : 'span',
       array(
         'href'  => $uri,
         'class' => implode(' ', $classes),
         'title' => $title,
       ),
       array($icon, $name));
   }
 
   public function renderTag() {
     return id(new PHUITagView())
       ->setType(PHUITagView::TYPE_OBJECT)
       ->setShade($this->getTagColor())
       ->setIcon($this->getIcon())
       ->setHref($this->getURI())
       ->setName($this->getLinkName());
   }
 
   public function getLinkName() {
     switch ($this->getType()) {
       case PhabricatorPeopleUserPHIDType::TYPECONST:
         $name = $this->getName();
         break;
       default:
         $name = $this->getFullName();
         break;
     }
     return $name;
   }
 
   protected function getPHIDType() {
     $types = PhabricatorPHIDType::getAllTypes();
     return idx($types, $this->getType());
   }
 
 
 /* -(  PhabricatorPolicyInterface  )----------------------------------------- */
 
 
   public function getCapabilities() {
     return array(
       PhabricatorPolicyCapability::CAN_VIEW,
     );
   }
 
   public function getPolicy($capability) {
     return PhabricatorPolicies::POLICY_PUBLIC;
   }
 
   public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
     // NOTE: Handles are always visible, they just don't get populated with
     // data if the user can't see the underlying object.
     return true;
   }
 
   public function describeAutomaticCapability($capability) {
     return null;
   }
 
 }
diff --git a/src/applications/phid/PhabricatorPHIDConstants.php b/src/applications/phid/PhabricatorPHIDConstants.php
index 1688042fb..6e6dfca6d 100644
--- a/src/applications/phid/PhabricatorPHIDConstants.php
+++ b/src/applications/phid/PhabricatorPHIDConstants.php
@@ -1,18 +1,18 @@
 <?php
 
-final class PhabricatorPHIDConstants {
+final class PhabricatorPHIDConstants extends Phobject {
 
   const PHID_TYPE_UNKNOWN = '????';
   const PHID_TYPE_MAGIC   = '!!!!';
   const PHID_TYPE_STRY    = 'STRY';
   const PHID_TYPE_TOBJ    = 'TOBJ';
   const PHID_TYPE_LEGB    = 'LEGB';
 
   const PHID_TYPE_XCMT    = 'XCMT';
 
   const PHID_TYPE_XOBJ    = 'XOBJ';
 
   const PHID_TYPE_VOID    = 'VOID';
   const PHID_VOID         = 'PHID-VOID-00000000000000000000';
 
 }
diff --git a/src/applications/phid/handle/view/PhabricatorHandleObjectSelectorDataView.php b/src/applications/phid/handle/view/PhabricatorHandleObjectSelectorDataView.php
index ac0a31244..772374644 100644
--- a/src/applications/phid/handle/view/PhabricatorHandleObjectSelectorDataView.php
+++ b/src/applications/phid/handle/view/PhabricatorHandleObjectSelectorDataView.php
@@ -1,19 +1,19 @@
 <?php
 
-final class PhabricatorHandleObjectSelectorDataView {
+final class PhabricatorHandleObjectSelectorDataView extends Phobject {
 
   private $handle;
 
   public function __construct($handle) {
     $this->handle = $handle;
   }
 
   public function renderData() {
     $handle = $this->handle;
     return array(
       'phid' => $handle->getPHID(),
       'name' => $handle->getFullName(),
       'uri'  => $handle->getURI(),
     );
   }
 }
diff --git a/src/applications/phid/query/PhabricatorObjectListQuery.php b/src/applications/phid/query/PhabricatorObjectListQuery.php
index 0d7d95e1c..c4bf0e9bb 100644
--- a/src/applications/phid/query/PhabricatorObjectListQuery.php
+++ b/src/applications/phid/query/PhabricatorObjectListQuery.php
@@ -1,150 +1,150 @@
 <?php
 
-final class PhabricatorObjectListQuery {
+final class PhabricatorObjectListQuery extends Phobject {
 
   private $viewer;
   private $objectList;
   private $allowedTypes = array();
   private $allowPartialResults;
 
   public function setAllowPartialResults($allow_partial_results) {
     $this->allowPartialResults = $allow_partial_results;
     return $this;
   }
 
   public function getAllowPartialResults() {
     return $this->allowPartialResults;
   }
 
   public function setAllowedTypes(array $allowed_types) {
     $this->allowedTypes = $allowed_types;
     return $this;
   }
 
   public function getAllowedTypes() {
     return $this->allowedTypes;
   }
 
   public function setViewer(PhabricatorUser $viewer) {
     $this->viewer = $viewer;
     return $this;
   }
 
   public function getViewer() {
     return $this->viewer;
   }
 
   public function setObjectList($object_list) {
     $this->objectList = $object_list;
     return $this;
   }
 
   public function getObjectList() {
     return $this->objectList;
   }
 
   public function execute() {
     $names = $this->getObjectList();
     $names = array_unique(array_filter(preg_split('/[\s,]+/', $names)));
 
     $objects = $this->loadObjects($names);
 
     $types = array();
     foreach ($objects as $name => $object) {
       $types[phid_get_type($object->getPHID())][] = $name;
     }
 
     $invalid = array();
     if ($this->getAllowedTypes()) {
       $allowed = array_fuse($this->getAllowedTypes());
       foreach ($types as $type => $names_of_type) {
         if (empty($allowed[$type])) {
           $invalid[] = $names_of_type;
         }
       }
     }
     $invalid = array_mergev($invalid);
 
     $missing = array();
     foreach ($names as $name) {
       if (empty($objects[$name])) {
         $missing[] = $name;
       }
     }
 
     // NOTE: We could couple this less tightly with Differential, but it is
     // currently the only thing that uses it, and we'd have to add a lot of
     // extra API to loosen this. It's not clear that this will be useful
     // elsewhere any time soon, so let's cross that bridge when we come to it.
 
     if (!$this->getAllowPartialResults()) {
       if ($invalid && $missing) {
         throw new DifferentialFieldParseException(
           pht(
             'The objects you have listed include objects of the wrong '.
             'type (%s) and objects which do not exist (%s).',
             implode(', ', $invalid),
             implode(', ', $missing)));
       } else if ($invalid) {
         throw new DifferentialFieldParseException(
           pht(
             'The objects you have listed include objects of the wrong '.
             'type (%s).',
             implode(', ', $invalid)));
       } else if ($missing) {
         throw new DifferentialFieldParseException(
           pht(
             'The objects you have listed include objects which do not '.
             'exist (%s).',
             implode(', ', $missing)));
       }
     }
 
     return array_values(array_unique(mpull($objects, 'getPHID')));
   }
 
   private function loadObjects($names) {
     // First, try to load visible objects using monograms. This covers most
     // object types, but does not cover users or user email addresses.
     $query = id(new PhabricatorObjectQuery())
       ->setViewer($this->getViewer())
       ->withNames($names);
 
     $query->execute();
     $objects = $query->getNamedResults();
 
     $results = array();
     foreach ($names as $key => $name) {
       if (isset($objects[$name])) {
         $results[$name] = $objects[$name];
         unset($names[$key]);
       }
     }
 
     if ($names) {
       // We still have some symbols we haven't been able to resolve, so try to
       // load users. Try by username first...
       $users = id(new PhabricatorPeopleQuery())
         ->setViewer($this->getViewer())
         ->withUsernames($names)
         ->execute();
 
       $user_map = array();
       foreach ($users as $user) {
         $user_map[phutil_utf8_strtolower($user->getUsername())] = $user;
       }
 
       foreach ($names as $key => $name) {
         $normal_name = phutil_utf8_strtolower($name);
         if (isset($user_map[$normal_name])) {
           $results[$name] = $user_map[$normal_name];
           unset($names[$key]);
         }
       }
     }
 
     return $results;
   }
 
 
 }
diff --git a/src/applications/phid/storage/PhabricatorPHID.php b/src/applications/phid/storage/PhabricatorPHID.php
index d3c43bfc4..7acacc947 100644
--- a/src/applications/phid/storage/PhabricatorPHID.php
+++ b/src/applications/phid/storage/PhabricatorPHID.php
@@ -1,27 +1,27 @@
 <?php
 
-final class PhabricatorPHID {
+final class PhabricatorPHID extends Phobject {
 
   protected $phid;
   protected $phidType;
   protected $ownerPHID;
   protected $parentPHID;
 
   public static function generateNewPHID($type, $subtype = null) {
     if (!$type) {
       throw new Exception(pht('Can not generate PHID with no type.'));
     }
 
     if ($subtype === null) {
       $uniq_len = 20;
       $type_str = "{$type}";
     } else {
       $uniq_len = 15;
       $type_str = "{$type}-{$subtype}";
     }
 
     $uniq = Filesystem::readRandomCharacters($uniq_len);
     return "PHID-{$type_str}-{$uniq}";
   }
 
 }
diff --git a/src/applications/phid/type/PhabricatorPHIDType.php b/src/applications/phid/type/PhabricatorPHIDType.php
index 62107233e..7006d8bb4 100644
--- a/src/applications/phid/type/PhabricatorPHIDType.php
+++ b/src/applications/phid/type/PhabricatorPHIDType.php
@@ -1,239 +1,239 @@
 <?php
 
-abstract class PhabricatorPHIDType {
+abstract class PhabricatorPHIDType extends Phobject {
 
   final public function getTypeConstant() {
     $class = new ReflectionClass($this);
 
     $const = $class->getConstant('TYPECONST');
     if ($const === false) {
       throw new Exception(
         pht(
           '%s class "%s" must define a %s property.',
           __CLASS__,
           get_class($this),
           'TYPECONST'));
     }
 
     if (!is_string($const) || !preg_match('/^[A-Z]{4}$/', $const)) {
       throw new Exception(
         pht(
           '%s class "%s" has an invalid %s property. PHID '.
           'constants must be a four character uppercase string.',
           __CLASS__,
           get_class($this),
           'TYPECONST'));
     }
 
     return $const;
   }
 
   abstract public function getTypeName();
 
   public function newObject() {
     return null;
   }
 
   public function getTypeIcon() {
     // Default to the application icon if the type doesn't specify one.
     $application_class = $this->getPHIDTypeApplicationClass();
     if ($application_class) {
       $application = newv($application_class, array());
       return $application->getFontIcon();
     }
 
     return null;
   }
 
 
   /**
    * Get the class name for the application this type belongs to.
    *
    * @return string|null Class name of the corresponding application, or null
    *   if the type is not bound to an application.
    */
   public function getPHIDTypeApplicationClass() {
     // TODO: Some day this should probably be abstract, but for now it only
     // affects global search and there's no real burning need to go classify
     // every PHID type.
     return null;
   }
 
   /**
    * Build a @{class:PhabricatorPolicyAwareQuery} to load objects of this type
    * by PHID.
    *
    * If you can not build a single query which satisfies this requirement, you
    * can provide a dummy implementation for this method and overload
    * @{method:loadObjects} instead.
    *
    * @param PhabricatorObjectQuery Query being executed.
    * @param list<phid> PHIDs to load.
    * @return PhabricatorPolicyAwareQuery Query object which loads the
    *   specified PHIDs when executed.
    */
   abstract protected function buildQueryForObjects(
     PhabricatorObjectQuery $query,
     array $phids);
 
 
   /**
    * Load objects of this type, by PHID. For most PHID types, it is only
    * necessary to implement @{method:buildQueryForObjects} to get object
    * loading to work.
    *
    * @param PhabricatorObjectQuery Query being executed.
    * @param list<phid> PHIDs to load.
    * @return list<wild> Corresponding objects.
    */
   public function loadObjects(
     PhabricatorObjectQuery $query,
     array $phids) {
 
     $object_query = $this->buildQueryForObjects($query, $phids)
       ->setViewer($query->getViewer())
       ->setParentQuery($query);
 
     // If the user doesn't have permission to use the application at all,
     // just mark all the PHIDs as filtered. This primarily makes these
     // objects show up as "Restricted" instead of "Unknown" when loaded as
     // handles, which is technically true.
     if (!$object_query->canViewerUseQueryApplication()) {
       $object_query->addPolicyFilteredPHIDs(array_fuse($phids));
       return array();
     }
 
     return $object_query->execute();
   }
 
 
   /**
    * Populate provided handles with application-specific data, like titles and
    * URIs.
    *
    * NOTE: The `$handles` and `$objects` lists are guaranteed to be nonempty
    * and have the same keys: subclasses are expected to load information only
    * for handles with visible objects.
    *
    * Because of this guarantee, a safe implementation will typically look like*
    *
    *   foreach ($handles as $phid => $handle) {
    *     $object = $objects[$phid];
    *
    *     $handle->setStuff($object->getStuff());
    *     // ...
    *   }
    *
    * In general, an implementation should call `setName()` and `setURI()` on
    * each handle at a minimum. See @{class:PhabricatorObjectHandle} for other
    * handle properties.
    *
    * @param PhabricatorHandleQuery          Issuing query object.
    * @param list<PhabricatorObjectHandle>   Handles to populate with data.
    * @param list<Object>                    Objects for these PHIDs loaded by
    *                                        @{method:buildQueryForObjects()}.
    * @return void
    */
   abstract public function loadHandles(
     PhabricatorHandleQuery $query,
     array $handles,
     array $objects);
 
   public function canLoadNamedObject($name) {
     return false;
   }
 
   public function loadNamedObjects(
     PhabricatorObjectQuery $query,
     array $names) {
     throw new PhutilMethodNotImplementedException();
   }
 
 
   /**
    * Get all known PHID types.
    *
    * To get PHID types a given user has access to, see
    * @{method:getAllInstalledTypes}.
    *
    * @return dict<string, PhabricatorPHIDType> Map of type constants to types.
    */
-  public static function getAllTypes() {
+  final public static function getAllTypes() {
     static $types;
     if ($types === null) {
       $objects = id(new PhutilSymbolLoader())
         ->setAncestorClass(__CLASS__)
         ->loadObjects();
 
       $map = array();
       $original = array();
       foreach ($objects as $object) {
         $type = $object->getTypeConstant();
         if (isset($map[$type])) {
           $that_class = $original[$type];
           $this_class = get_class($object);
           throw new Exception(
             pht(
               "Two %s classes (%s, %s) both handle PHID type '%s'. ".
               "A type may be handled by only one class.",
               __CLASS__,
               $that_class,
               $this_class,
               $type));
         }
 
         $original[$type] = get_class($object);
         $map[$type] = $object;
       }
 
       $types = $map;
     }
 
     return $types;
   }
 
 
   /**
    * Get all PHID types of applications installed for a given viewer.
    *
    * @param PhabricatorUser Viewing user.
    * @return dict<string, PhabricatorPHIDType> Map of constants to installed
    *  types.
    */
   public static function getAllInstalledTypes(PhabricatorUser $viewer) {
     $all_types = self::getAllTypes();
 
     $installed_types = array();
 
     $app_classes = array();
     foreach ($all_types as $key => $type) {
       $app_class = $type->getPHIDTypeApplicationClass();
 
       if ($app_class === null) {
         // If the PHID type isn't bound to an application, include it as
         // installed.
         $installed_types[$key] = $type;
         continue;
       }
 
       // Otherwise, we need to check if this application is installed before
       // including the PHID type.
       $app_classes[$app_class][$key] = $type;
     }
 
     if ($app_classes) {
       $apps = id(new PhabricatorApplicationQuery())
         ->setViewer($viewer)
         ->withInstalled(true)
         ->withClasses(array_keys($app_classes))
         ->execute();
 
       foreach ($apps as $app_class => $app) {
         $installed_types += $app_classes[$app_class];
       }
     }
 
     return $installed_types;
   }
 
 }
diff --git a/src/applications/phid/type/__tests__/PhabricatorPHIDTypeTestCase.php b/src/applications/phid/type/__tests__/PhabricatorPHIDTypeTestCase.php
new file mode 100644
index 000000000..e7707095a
--- /dev/null
+++ b/src/applications/phid/type/__tests__/PhabricatorPHIDTypeTestCase.php
@@ -0,0 +1,10 @@
+<?php
+
+final class PhabricatorPHIDTypeTestCase extends PhutilTestCase {
+
+  public function testGetAllTypes() {
+    PhabricatorPHIDType::getAllTypes();
+    $this->assertTrue(true);
+  }
+
+}
diff --git a/src/applications/phid/view/PHUIHandleView.php b/src/applications/phid/view/PHUIHandleView.php
index db4b9e43a..a4b766a99 100644
--- a/src/applications/phid/view/PHUIHandleView.php
+++ b/src/applications/phid/view/PHUIHandleView.php
@@ -1,42 +1,52 @@
 <?php
 
 /**
  * Convenience class for rendering a single handle.
  *
  * This class simplifies rendering a single handle, and improves loading and
  * caching semantics in the rendering pipeline by loading data at the last
  * moment.
  */
 
 final class PHUIHandleView
   extends AphrontView {
 
   private $handleList;
   private $handlePHID;
   private $asTag;
+  private $useShortName;
 
   public function setHandleList(PhabricatorHandleList $list) {
     $this->handleList = $list;
     return $this;
   }
 
   public function setHandlePHID($phid) {
     $this->handlePHID = $phid;
     return $this;
   }
 
   public function setAsTag($tag) {
     $this->asTag = $tag;
     return $this;
   }
 
+  public function setUseShortName($short) {
+    $this->useShortName = $short;
+    return $this;
+  }
+
   public function render() {
     $handle = $this->handleList[$this->handlePHID];
     if ($this->asTag) {
       return $handle->renderTag();
     } else {
-      return $handle->renderLink();
+      if ($this->useShortName) {
+        return $handle->renderLink($handle->getName());
+      } else {
+        return $handle->renderLink();
+      }
     }
   }
 
 }
diff --git a/src/applications/pholio/application/PhabricatorPholioApplication.php b/src/applications/pholio/application/PhabricatorPholioApplication.php
index 801a2de4e..52309ba07 100644
--- a/src/applications/pholio/application/PhabricatorPholioApplication.php
+++ b/src/applications/pholio/application/PhabricatorPholioApplication.php
@@ -1,98 +1,102 @@
 <?php
 
 final class PhabricatorPholioApplication extends PhabricatorApplication {
 
   public function getName() {
     return pht('Pholio');
   }
 
   public function getBaseURI() {
     return '/pholio/';
   }
 
   public function getShortDescription() {
     return pht('Review Mocks and Design');
   }
 
   public function getFontIcon() {
     return 'fa-camera-retro';
   }
 
   public function getTitleGlyph() {
     return "\xE2\x9D\xA6";
   }
 
   public function getFlavorText() {
     return pht('Things before they were cool.');
   }
 
   public function getEventListeners() {
     return array(
       new PholioActionMenuEventListener(),
     );
   }
 
   public function getRemarkupRules() {
     return array(
       new PholioRemarkupRule(),
     );
   }
 
   public function getRoutes() {
     return array(
       '/M(?P<id>[1-9]\d*)(?:/(?P<imageID>\d+)/)?' => 'PholioMockViewController',
       '/pholio/' => array(
         '(?:query/(?P<queryKey>[^/]+)/)?' => 'PholioMockListController',
         'new/'                  => 'PholioMockEditController',
         'edit/(?P<id>\d+)/'     => 'PholioMockEditController',
         'comment/(?P<id>\d+)/'  => 'PholioMockCommentController',
         'inline/' => array(
           '(?:(?P<id>\d+)/)?' => 'PholioInlineController',
           'list/(?P<id>\d+)/' => 'PholioInlineListController',
         ),
         'image/' => array(
           'upload/' => 'PholioImageUploadController',
         ),
       ),
     );
   }
 
   public function getQuickCreateItems(PhabricatorUser $viewer) {
     $items = array();
 
     $item = id(new PHUIListItemView())
       ->setName(pht('Pholio Mock'))
       ->setIcon('fa-picture-o')
       ->setHref($this->getBaseURI().'new/');
     $items[] = $item;
 
     return $items;
   }
 
   protected function getCustomCapabilities() {
     return array(
-      PholioDefaultViewCapability::CAPABILITY => array(),
-      PholioDefaultEditCapability::CAPABILITY => array(),
+      PholioDefaultViewCapability::CAPABILITY => array(
+        'template' => PholioMockPHIDType::TYPECONST,
+      ),
+      PholioDefaultEditCapability::CAPABILITY => array(
+        'template' => PholioMockPHIDType::TYPECONST,
+      ),
     );
   }
 
   public function getMailCommandObjects() {
     return array(
       'mock' => array(
         'name' => pht('Email Commands: Mocks'),
         'header' => pht('Interacting with Pholio Mocks'),
         'object' => new PholioMock(),
         'summary' => pht(
           'This page documents the commands you can use to interact with '.
           'mocks in Pholio.'),
       ),
     );
   }
 
   public function getApplicationSearchDocumentTypes() {
     return array(
       PholioMockPHIDType::TYPECONST,
     );
   }
 
 }
diff --git a/src/applications/pholio/controller/PholioMockEditController.php b/src/applications/pholio/controller/PholioMockEditController.php
index 3ed2cc077..728c7b29d 100644
--- a/src/applications/pholio/controller/PholioMockEditController.php
+++ b/src/applications/pholio/controller/PholioMockEditController.php
@@ -1,395 +1,400 @@
 <?php
 
 final class PholioMockEditController extends PholioController {
 
   private $id;
 
   public function willProcessRequest(array $data) {
     $this->id = idx($data, 'id');
   }
 
   public function processRequest() {
     $request = $this->getRequest();
     $user = $request->getUser();
 
     if ($this->id) {
       $mock = id(new PholioMockQuery())
         ->setViewer($user)
         ->needImages(true)
         ->requireCapabilities(
           array(
             PhabricatorPolicyCapability::CAN_VIEW,
             PhabricatorPolicyCapability::CAN_EDIT,
           ))
         ->withIDs(array($this->id))
         ->executeOne();
 
       if (!$mock) {
         return new Aphront404Response();
       }
 
       $title = pht('Edit Mock');
 
       $is_new = false;
       $mock_images = $mock->getImages();
       $files = mpull($mock_images, 'getFile');
       $mock_images = mpull($mock_images, null, 'getFilePHID');
     } else {
       $mock = PholioMock::initializeNewMock($user);
 
       $title = pht('Create Mock');
 
       $is_new = true;
       $files = array();
       $mock_images = array();
     }
 
     if ($is_new) {
       $v_projects = array();
     } else {
       $v_projects = PhabricatorEdgeQuery::loadDestinationPHIDs(
         $mock->getPHID(),
         PhabricatorProjectObjectHasProjectEdgeType::EDGECONST);
       $v_projects = array_reverse($v_projects);
     }
 
     $e_name = true;
     $e_images = count($mock_images) ? null : true;
     $errors = array();
     $posted_mock_images = array();
 
     $v_name = $mock->getName();
     $v_desc = $mock->getDescription();
     $v_status = $mock->getStatus();
     $v_view = $mock->getViewPolicy();
     $v_edit = $mock->getEditPolicy();
     $v_cc = PhabricatorSubscribersQuery::loadSubscribersForPHID(
       $mock->getPHID());
+    $v_space = $mock->getSpacePHID();
 
     if ($request->isFormPost()) {
       $xactions = array();
 
       $type_name = PholioTransaction::TYPE_NAME;
       $type_desc = PholioTransaction::TYPE_DESCRIPTION;
       $type_status = PholioTransaction::TYPE_STATUS;
       $type_view = PhabricatorTransactions::TYPE_VIEW_POLICY;
       $type_edit = PhabricatorTransactions::TYPE_EDIT_POLICY;
       $type_cc   = PhabricatorTransactions::TYPE_SUBSCRIBERS;
+      $type_space = PhabricatorTransactions::TYPE_SPACE;
 
       $v_name = $request->getStr('name');
       $v_desc = $request->getStr('description');
       $v_status = $request->getStr('status');
       $v_view = $request->getStr('can_view');
       $v_edit = $request->getStr('can_edit');
       $v_cc   = $request->getArr('cc');
       $v_projects = $request->getArr('projects');
+      $v_space = $request->getStr('spacePHID');
 
       $mock_xactions = array();
       $mock_xactions[$type_name] = $v_name;
       $mock_xactions[$type_desc] = $v_desc;
       $mock_xactions[$type_status] = $v_status;
       $mock_xactions[$type_view] = $v_view;
       $mock_xactions[$type_edit] = $v_edit;
       $mock_xactions[$type_cc]   = array('=' => $v_cc);
+      $mock_xactions[$type_space] = $v_space;
 
       if (!strlen($request->getStr('name'))) {
         $e_name = pht('Required');
         $errors[] = pht('You must give the mock a name.');
       }
 
       $file_phids = $request->getArr('file_phids');
       if ($file_phids) {
         $files = id(new PhabricatorFileQuery())
           ->setViewer($user)
           ->withPHIDs($file_phids)
           ->execute();
         $files = mpull($files, null, 'getPHID');
         $files = array_select_keys($files, $file_phids);
       } else {
         $files = array();
       }
 
       if (!$files) {
         $e_images = pht('Required');
         $errors[] = pht('You must add at least one image to the mock.');
       } else {
         $mock->setCoverPHID(head($files)->getPHID());
       }
 
       foreach ($mock_xactions as $type => $value) {
         $xactions[$type] = id(new PholioTransaction())
           ->setTransactionType($type)
           ->setNewValue($value);
       }
 
       $order = $request->getStrList('imageOrder');
       $sequence_map = array_flip($order);
       $replaces = $request->getArr('replaces');
       $replaces_map = array_flip($replaces);
 
       /**
        * Foreach file posted, check to see whether we are replacing an image,
        * adding an image, or simply updating image metadata. Create
        * transactions for these cases as appropos.
        */
       foreach ($files as $file_phid => $file) {
         $replaces_image_phid = null;
         if (isset($replaces_map[$file_phid])) {
           $old_file_phid = $replaces_map[$file_phid];
           if ($old_file_phid != $file_phid) {
             $old_image = idx($mock_images, $old_file_phid);
             if ($old_image) {
               $replaces_image_phid = $old_image->getPHID();
             }
           }
         }
 
         $existing_image = idx($mock_images, $file_phid);
 
         $title = (string)$request->getStr('title_'.$file_phid);
         $description = (string)$request->getStr('description_'.$file_phid);
         $sequence = $sequence_map[$file_phid];
 
         if ($replaces_image_phid) {
           $replace_image = id(new PholioImage())
             ->setReplacesImagePHID($replaces_image_phid)
             ->setFilePhid($file_phid)
             ->attachFile($file)
             ->setName(strlen($title) ? $title : $file->getName())
             ->setDescription($description)
             ->setSequence($sequence);
           $xactions[] = id(new PholioTransaction())
             ->setTransactionType(
               PholioTransaction::TYPE_IMAGE_REPLACE)
             ->setNewValue($replace_image);
           $posted_mock_images[] = $replace_image;
         } else if (!$existing_image) { // this is an add
           $add_image = id(new PholioImage())
             ->setFilePhid($file_phid)
             ->attachFile($file)
             ->setName(strlen($title) ? $title : $file->getName())
             ->setDescription($description)
             ->setSequence($sequence);
           $xactions[] = id(new PholioTransaction())
             ->setTransactionType(PholioTransaction::TYPE_IMAGE_FILE)
             ->setNewValue(
               array('+' => array($add_image)));
           $posted_mock_images[] = $add_image;
         } else {
           $xactions[] = id(new PholioTransaction())
             ->setTransactionType(PholioTransaction::TYPE_IMAGE_NAME)
             ->setNewValue(
               array($existing_image->getPHID() => $title));
           $xactions[] = id(new PholioTransaction())
             ->setTransactionType(
               PholioTransaction::TYPE_IMAGE_DESCRIPTION)
               ->setNewValue(
                 array($existing_image->getPHID() => $description));
           $xactions[] = id(new PholioTransaction())
             ->setTransactionType(
               PholioTransaction::TYPE_IMAGE_SEQUENCE)
               ->setNewValue(
                 array($existing_image->getPHID() => $sequence));
 
           $posted_mock_images[] = $existing_image;
         }
       }
       foreach ($mock_images as $file_phid => $mock_image) {
         if (!isset($files[$file_phid]) && !isset($replaces[$file_phid])) {
           // this is an outright delete
           $xactions[] = id(new PholioTransaction())
             ->setTransactionType(PholioTransaction::TYPE_IMAGE_FILE)
             ->setNewValue(
               array('-' => array($mock_image)));
         }
       }
 
       if (!$errors) {
         $proj_edge_type = PhabricatorProjectObjectHasProjectEdgeType::EDGECONST;
         $xactions[] = id(new PholioTransaction())
           ->setTransactionType(PhabricatorTransactions::TYPE_EDGE)
           ->setMetadataValue('edge:type', $proj_edge_type)
           ->setNewValue(array('=' => array_fuse($v_projects)));
 
         $mock->openTransaction();
         $editor = id(new PholioMockEditor())
           ->setContentSourceFromRequest($request)
           ->setContinueOnNoEffect(true)
           ->setActor($user);
 
         $xactions = $editor->applyTransactions($mock, $xactions);
 
         $mock->saveTransaction();
 
         return id(new AphrontRedirectResponse())
           ->setURI('/M'.$mock->getID());
       }
     }
 
     if ($this->id) {
       $submit = id(new AphrontFormSubmitControl())
         ->addCancelButton('/M'.$this->id)
         ->setValue(pht('Save'));
     } else {
       $submit = id(new AphrontFormSubmitControl())
         ->addCancelButton($this->getApplicationURI())
         ->setValue(pht('Create'));
     }
 
     $policies = id(new PhabricatorPolicyQuery())
       ->setViewer($user)
       ->setObject($mock)
       ->execute();
 
     // NOTE: Make this show up correctly on the rendered form.
     $mock->setViewPolicy($v_view);
     $mock->setEditPolicy($v_edit);
 
     $image_elements = array();
     if ($posted_mock_images) {
       $display_mock_images = $posted_mock_images;
     } else {
       $display_mock_images = $mock_images;
     }
     foreach ($display_mock_images as $mock_image) {
       $image_elements[] = id(new PholioUploadedImageView())
         ->setUser($user)
         ->setImage($mock_image)
         ->setReplacesPHID($mock_image->getFilePHID());
     }
 
     $list_id = celerity_generate_unique_node_id();
     $drop_id = celerity_generate_unique_node_id();
     $order_id = celerity_generate_unique_node_id();
 
     $list_control = phutil_tag(
       'div',
       array(
         'id' => $list_id,
         'class' => 'pholio-edit-list',
       ),
       $image_elements);
 
     $drop_control = phutil_tag(
       'div',
       array(
         'id' => $drop_id,
         'class' => 'pholio-edit-drop',
       ),
       pht('Drag and drop images here to add them to the mock.'));
 
     $order_control = phutil_tag(
       'input',
       array(
         'type' => 'hidden',
         'name' => 'imageOrder',
         'id' => $order_id,
       ));
 
     Javelin::initBehavior(
       'pholio-mock-edit',
       array(
         'listID' => $list_id,
         'dropID' => $drop_id,
         'orderID' => $order_id,
         'uploadURI' => '/file/dropupload/',
         'renderURI' => $this->getApplicationURI('image/upload/'),
         'pht' => array(
           'uploading' => pht('Uploading Image...'),
           'uploaded' => pht('Upload Complete...'),
           'undo' => pht('Undo'),
           'removed' => pht('This image will be removed from the mock.'),
         ),
       ));
 
     require_celerity_resource('pholio-edit-css');
     $form = id(new AphrontFormView())
       ->setUser($user)
       ->appendChild($order_control)
       ->appendChild(
         id(new AphrontFormTextControl())
         ->setName('name')
         ->setValue($v_name)
         ->setLabel(pht('Name'))
         ->setError($e_name))
       ->appendChild(
         id(new PhabricatorRemarkupControl())
         ->setName('description')
         ->setValue($v_desc)
         ->setLabel(pht('Description'))
         ->setUser($user));
 
     if ($this->id) {
       $form->appendChild(
         id(new AphrontFormSelectControl())
         ->setLabel(pht('Status'))
         ->setName('status')
         ->setValue($mock->getStatus())
         ->setOptions($mock->getStatuses()));
     } else {
       $form->addHiddenInput('status', 'open');
     }
 
     $form
       ->appendControl(
         id(new AphrontFormTokenizerControl())
           ->setLabel(pht('Projects'))
           ->setName('projects')
           ->setValue($v_projects)
           ->setDatasource(new PhabricatorProjectDatasource()))
       ->appendControl(
         id(new AphrontFormTokenizerControl())
           ->setLabel(pht('Subscribers'))
           ->setName('cc')
           ->setValue($v_cc)
           ->setUser($user)
           ->setDatasource(new PhabricatorMetaMTAMailableDatasource()))
       ->appendChild(
         id(new AphrontFormPolicyControl())
           ->setUser($user)
           ->setCapability(PhabricatorPolicyCapability::CAN_VIEW)
           ->setPolicyObject($mock)
           ->setPolicies($policies)
+          ->setSpacePHID($v_space)
           ->setName('can_view'))
       ->appendChild(
         id(new AphrontFormPolicyControl())
           ->setUser($user)
           ->setCapability(PhabricatorPolicyCapability::CAN_EDIT)
           ->setPolicyObject($mock)
           ->setPolicies($policies)
           ->setName('can_edit'))
       ->appendChild(
         id(new AphrontFormMarkupControl())
           ->setValue($list_control))
       ->appendChild(
         id(new AphrontFormMarkupControl())
           ->setValue($drop_control)
           ->setError($e_images))
       ->appendChild($submit);
 
     $form_box = id(new PHUIObjectBoxView())
       ->setHeaderText($title)
       ->setFormErrors($errors)
       ->setForm($form);
 
     $crumbs = $this->buildApplicationCrumbs();
     if (!$is_new) {
       $crumbs->addTextCrumb($mock->getMonogram(), '/'.$mock->getMonogram());
     }
     $crumbs->addTextCrumb($title);
 
     $content = array(
       $crumbs,
       $form_box,
     );
 
     $this->addExtraQuicksandConfig(
       array('mockEditConfig' => true));
     return $this->buildApplicationPage(
       $content,
       array(
         'title' => $title,
       ));
   }
 
 }
diff --git a/src/applications/pholio/query/PholioMockSearchEngine.php b/src/applications/pholio/query/PholioMockSearchEngine.php
index 26b22034a..23c1decad 100644
--- a/src/applications/pholio/query/PholioMockSearchEngine.php
+++ b/src/applications/pholio/query/PholioMockSearchEngine.php
@@ -1,133 +1,130 @@
 <?php
 
 final class PholioMockSearchEngine extends PhabricatorApplicationSearchEngine {
 
   public function getResultTypeDescription() {
     return pht('Pholio Mocks');
   }
 
   public function getApplicationClassName() {
     return 'PhabricatorPholioApplication';
   }
 
   public function newQuery() {
     return id(new PholioMockQuery())
       ->needCoverFiles(true)
       ->needImages(true)
       ->needTokenCounts(true);
   }
 
   protected function buildCustomSearchFields() {
     return array(
       id(new PhabricatorSearchUsersField())
         ->setKey('authorPHIDs')
         ->setAliases(array('authors'))
         ->setLabel(pht('Authors')),
       id(new PhabricatorSearchCheckboxesField())
         ->setKey('statuses')
         ->setLabel(pht('Status'))
         ->setOptions(
           id(new PholioMock())
             ->getStatuses()),
     );
   }
 
   protected function buildQueryFromParameters(array $map) {
     $query = $this->newQuery();
 
     if ($map['authorPHIDs']) {
       $query->withAuthorPHIDs($map['authorPHIDs']);
     }
 
     if ($map['statuses']) {
       $query->withStatuses($map['statuses']);
     }
 
     return $query;
   }
 
   protected function getURI($path) {
     return '/pholio/'.$path;
   }
 
   protected function getBuiltinQueryNames() {
     $names = array(
       'open' => pht('Open Mocks'),
       'all' => pht('All Mocks'),
     );
 
     if ($this->requireViewer()->isLoggedIn()) {
       $names['authored'] = pht('Authored');
     }
 
     return $names;
   }
 
   public function buildSavedQueryFromBuiltin($query_key) {
     $query = $this->newSavedQuery();
     $query->setQueryKey($query_key);
 
     switch ($query_key) {
       case 'open':
         return $query->setParameter(
           'statuses',
           array('open'));
       case 'all':
         return $query;
       case 'authored':
         return $query->setParameter(
           'authorPHIDs',
           array($this->requireViewer()->getPHID()));
     }
 
     return parent::buildSavedQueryFromBuiltin($query_key);
   }
 
-  protected function getRequiredHandlePHIDsForResultList(
-    array $mocks,
-    PhabricatorSavedQuery $query) {
-    return mpull($mocks, 'getAuthorPHID');
-  }
-
   protected function renderResultList(
     array $mocks,
     PhabricatorSavedQuery $query,
     array $handles) {
     assert_instances_of($mocks, 'PholioMock');
 
     $viewer = $this->requireViewer();
+    $handles = $viewer->loadHandles(mpull($mocks, 'getAuthorPHID'));
 
     $xform = PhabricatorFileTransform::getTransformByKey(
       PhabricatorFileThumbnailTransform::TRANSFORM_PINBOARD);
 
     $board = new PHUIPinboardView();
     foreach ($mocks as $mock) {
 
       $image = $mock->getCoverFile();
       $image_uri = $image->getURIForTransform($xform);
       list($x, $y) = $xform->getTransformedDimensions($image);
 
       $header = 'M'.$mock->getID().' '.$mock->getName();
       $item = id(new PHUIPinboardItemView())
+        ->setUser($viewer)
         ->setHeader($header)
+        ->setObject($mock)
         ->setURI('/M'.$mock->getID())
         ->setImageURI($image_uri)
         ->setImageSize($x, $y)
         ->setDisabled($mock->isClosed())
         ->addIconCount('fa-picture-o', count($mock->getImages()))
         ->addIconCount('fa-trophy', $mock->getTokenCount());
 
       if ($mock->getAuthorPHID()) {
         $author_handle = $handles[$mock->getAuthorPHID()];
         $datetime = phabricator_date($mock->getDateCreated(), $viewer);
         $item->appendChild(
           pht('By %s on %s', $author_handle->renderLink(), $datetime));
       }
 
       $board->addItem($item);
     }
 
     return $board;
   }
 
 }
diff --git a/src/applications/pholio/remarkup/PholioRemarkupRule.php b/src/applications/pholio/remarkup/PholioRemarkupRule.php
index 2b1ca0f87..00025b632 100644
--- a/src/applications/pholio/remarkup/PholioRemarkupRule.php
+++ b/src/applications/pholio/remarkup/PholioRemarkupRule.php
@@ -1,85 +1,88 @@
 <?php
 
 final class PholioRemarkupRule extends PhabricatorObjectRemarkupRule {
 
   protected function getObjectNamePrefix() {
     return 'M';
   }
 
   protected function getObjectIDPattern() {
     // Match "M123", "M123/456", and "M123/456/". Users can hit the latter
     // forms when clicking comment anchors on a mock page.
     return '[1-9]\d*(?:/[1-9]\d*/?)?';
   }
 
   protected function getObjectHref(
     $object,
     PhabricatorObjectHandle $handle,
     $id) {
 
     $href = $handle->getURI();
 
     // If the ID has a `M123/456` component, link to that specific image.
     $id = explode('/', $id);
     if (isset($id[1])) {
       $href = $href.'/'.$id[1].'/';
     }
 
     if ($this->getEngine()->getConfig('uri.full')) {
       $href = PhabricatorEnv::getURI($href);
     }
 
     return $href;
   }
 
   protected function loadObjects(array $ids) {
     // Strip off any image ID components of the URI.
     $map = array();
     foreach ($ids as $id) {
       $map[head(explode('/', $id))][] = $id;
     }
 
     $viewer = $this->getEngine()->getConfig('viewer');
     $mocks = id(new PholioMockQuery())
       ->setViewer($viewer)
       ->needCoverFiles(true)
       ->needImages(true)
       ->needTokenCounts(true)
       ->withIDs(array_keys($map))
       ->execute();
 
     $results = array();
     foreach ($mocks as $mock) {
       $ids = idx($map, $mock->getID(), array());
       foreach ($ids as $id) {
         $results[$id] = $mock;
       }
     }
 
     return $results;
   }
 
   protected function renderObjectEmbed(
     $object,
     PhabricatorObjectHandle $handle,
     $options) {
 
+    $viewer = $this->getEngine()->getConfig('viewer');
+
     $embed_mock = id(new PholioMockEmbedView())
+      ->setUser($viewer)
       ->setMock($object);
 
     if (strlen($options)) {
       $parser = new PhutilSimpleOptions();
       $opts = $parser->parse(substr($options, 1));
 
       if (isset($opts['image'])) {
         $images = array_unique(
           explode('&', preg_replace('/\s+/', '', $opts['image'])));
 
         $embed_mock->setImages($images);
       }
     }
 
     return $embed_mock->render();
   }
 
 }
diff --git a/src/applications/pholio/storage/PholioMock.php b/src/applications/pholio/storage/PholioMock.php
index d229aa802..22c101570 100644
--- a/src/applications/pholio/storage/PholioMock.php
+++ b/src/applications/pholio/storage/PholioMock.php
@@ -1,311 +1,323 @@
 <?php
 
 final class PholioMock extends PholioDAO
   implements
     PhabricatorMarkupInterface,
     PhabricatorPolicyInterface,
     PhabricatorSubscribableInterface,
     PhabricatorTokenReceiverInterface,
     PhabricatorFlaggableInterface,
     PhabricatorApplicationTransactionInterface,
     PhabricatorProjectInterface,
-    PhabricatorDestructibleInterface {
+    PhabricatorDestructibleInterface,
+    PhabricatorSpacesInterface {
 
   const MARKUP_FIELD_DESCRIPTION  = 'markup:description';
 
   const STATUS_OPEN = 'open';
   const STATUS_CLOSED = 'closed';
 
   protected $authorPHID;
   protected $viewPolicy;
   protected $editPolicy;
 
   protected $name;
   protected $originalName;
   protected $description;
   protected $coverPHID;
   protected $mailKey;
   protected $status;
+  protected $spacePHID;
 
   private $images = self::ATTACHABLE;
   private $allImages = self::ATTACHABLE;
   private $coverFile = self::ATTACHABLE;
   private $tokenCount = self::ATTACHABLE;
 
   public static function initializeNewMock(PhabricatorUser $actor) {
     $app = id(new PhabricatorApplicationQuery())
       ->setViewer($actor)
       ->withClasses(array('PhabricatorPholioApplication'))
       ->executeOne();
 
     $view_policy = $app->getPolicy(PholioDefaultViewCapability::CAPABILITY);
     $edit_policy = $app->getPolicy(PholioDefaultEditCapability::CAPABILITY);
 
     return id(new PholioMock())
       ->setAuthorPHID($actor->getPHID())
       ->attachImages(array())
       ->setStatus(self::STATUS_OPEN)
       ->setViewPolicy($view_policy)
-      ->setEditPolicy($edit_policy);
+      ->setEditPolicy($edit_policy)
+      ->setSpacePHID($actor->getDefaultSpacePHID());
   }
 
   public function getMonogram() {
     return 'M'.$this->getID();
   }
 
   protected function getConfiguration() {
     return array(
       self::CONFIG_AUX_PHID => true,
       self::CONFIG_COLUMN_SCHEMA => array(
         'name' => 'text128',
         'description' => 'text',
         'originalName' => 'text128',
         'mailKey' => 'bytes20',
         'status' => 'text12',
       ),
       self::CONFIG_KEY_SCHEMA => array(
         'key_phid' => null,
         'phid' => array(
           'columns' => array('phid'),
           'unique' => true,
         ),
         'authorPHID' => array(
           'columns' => array('authorPHID'),
         ),
       ),
     ) + parent::getConfiguration();
   }
 
   public function generatePHID() {
     return PhabricatorPHID::generateNewPHID('MOCK');
   }
 
   public function save() {
     if (!$this->getMailKey()) {
       $this->setMailKey(Filesystem::readRandomCharacters(20));
     }
     return parent::save();
   }
 
   /**
    * These should be the images currently associated with the Mock.
    */
   public function attachImages(array $images) {
     assert_instances_of($images, 'PholioImage');
     $this->images = $images;
     return $this;
   }
 
   public function getImages() {
     $this->assertAttached($this->images);
     return $this->images;
   }
 
   /**
    * These should be *all* images associated with the Mock. This includes
    * images which have been removed and / or replaced from the Mock.
    */
   public function attachAllImages(array $images) {
     assert_instances_of($images, 'PholioImage');
     $this->allImages = $images;
     return $this;
   }
 
   public function getAllImages() {
     $this->assertAttached($this->images);
     return $this->allImages;
   }
 
   public function attachCoverFile(PhabricatorFile $file) {
     $this->coverFile = $file;
     return $this;
   }
 
   public function getCoverFile() {
     $this->assertAttached($this->coverFile);
     return $this->coverFile;
   }
 
   public function getTokenCount() {
     $this->assertAttached($this->tokenCount);
     return $this->tokenCount;
   }
 
   public function attachTokenCount($count) {
     $this->tokenCount = $count;
     return $this;
   }
 
   public function getImageHistorySet($image_id) {
     $images = $this->getAllImages();
     $images = mpull($images, null, 'getID');
     $selected_image = $images[$image_id];
 
     $replace_map = mpull($images, null, 'getReplacesImagePHID');
     $phid_map = mpull($images, null, 'getPHID');
 
     // find the earliest image
     $image = $selected_image;
     while (isset($phid_map[$image->getReplacesImagePHID()])) {
       $image = $phid_map[$image->getReplacesImagePHID()];
     }
 
     // now build history moving forward
     $history = array($image->getID() => $image);
     while (isset($replace_map[$image->getPHID()])) {
       $image = $replace_map[$image->getPHID()];
       $history[$image->getID()] = $image;
     }
 
     return $history;
   }
 
   public function getStatuses() {
     $options = array();
     $options[self::STATUS_OPEN] = pht('Open');
     $options[self::STATUS_CLOSED] = pht('Closed');
     return $options;
   }
 
   public function isClosed() {
     return ($this->getStatus() == 'closed');
   }
 
 
 /* -(  PhabricatorSubscribableInterface Implementation  )-------------------- */
 
 
   public function isAutomaticallySubscribed($phid) {
     return ($this->authorPHID == $phid);
   }
 
   public function shouldShowSubscribersProperty() {
     return true;
   }
 
   public function shouldAllowSubscription($phid) {
     return true;
   }
 
 
 /* -(  PhabricatorPolicyInterface Implementation  )-------------------------- */
 
 
   public function getCapabilities() {
     return array(
       PhabricatorPolicyCapability::CAN_VIEW,
       PhabricatorPolicyCapability::CAN_EDIT,
     );
   }
 
   public function getPolicy($capability) {
     switch ($capability) {
       case PhabricatorPolicyCapability::CAN_VIEW:
         return $this->getViewPolicy();
       case PhabricatorPolicyCapability::CAN_EDIT:
         return $this->getEditPolicy();
     }
   }
 
   public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
     return ($viewer->getPHID() == $this->getAuthorPHID());
   }
 
   public function describeAutomaticCapability($capability) {
     return pht("A mock's owner can always view and edit it.");
   }
 
 
 /* -(  PhabricatorMarkupInterface  )----------------------------------------- */
 
 
   public function getMarkupFieldKey($field) {
     $hash = PhabricatorHash::digest($this->getMarkupText($field));
     return 'M:'.$hash;
   }
 
   public function newMarkupEngine($field) {
     return PhabricatorMarkupEngine::newMarkupEngine(array());
   }
 
   public function getMarkupText($field) {
     if ($this->getDescription()) {
       $description = $this->getDescription();
     } else {
       $description = pht('No Description Given');
     }
 
     return $description;
   }
 
   public function didMarkupText($field, $output, PhutilMarkupEngine $engine) {
     require_celerity_resource('phabricator-remarkup-css');
     return phutil_tag(
       'div',
       array(
         'class' => 'phabricator-remarkup',
       ),
       $output);
   }
 
   public function shouldUseMarkupCache($field) {
     return (bool)$this->getID();
   }
 
 
 /* -(  PhabricatorApplicationTransactionInterface  )------------------------- */
 
 
   public function getApplicationTransactionEditor() {
     return new PholioMockEditor();
   }
 
   public function getApplicationTransactionObject() {
     return $this;
   }
 
   public function getApplicationTransactionTemplate() {
     return new PholioTransaction();
   }
 
   public function willRenderTimeline(
     PhabricatorApplicationTransactionView $timeline,
     AphrontRequest $request) {
 
     PholioMockQuery::loadImages(
       $request->getUser(),
       array($this),
       $need_inline_comments = true);
     $timeline->setMock($this);
     return $timeline;
   }
 
 /* -(  PhabricatorTokenReceiverInterface  )---------------------------------- */
 
 
   public function getUsersToNotifyOfTokenGiven() {
     return array(
       $this->getAuthorPHID(),
     );
   }
 
 
 /* -(  PhabricatorDestructibleInterface  )----------------------------------- */
 
 
   public function destroyObjectPermanently(
     PhabricatorDestructionEngine $engine) {
 
     $this->openTransaction();
       $images = id(new PholioImage())->loadAllWhere(
         'mockID = %d',
         $this->getID());
       foreach ($images as $image) {
         $image->delete();
       }
 
       $this->delete();
     $this->saveTransaction();
   }
 
+
+/* -(  PhabricatorSpacesInterface  )----------------------------------------- */
+
+
+  public function getSpacePHID() {
+    return $this->spacePHID;
+  }
+
+
 }
diff --git a/src/applications/pholio/view/PholioMockEmbedView.php b/src/applications/pholio/view/PholioMockEmbedView.php
index 3429cfd56..88f2f2ac5 100644
--- a/src/applications/pholio/view/PholioMockEmbedView.php
+++ b/src/applications/pholio/view/PholioMockEmbedView.php
@@ -1,61 +1,63 @@
 <?php
 
 final class PholioMockEmbedView extends AphrontView {
 
   private $mock;
   private $images = array();
 
   public function setMock(PholioMock $mock) {
     $this->mock = $mock;
     return $this;
   }
 
   public function setImages(array $images) {
     $this->images = $images;
     return $this;
   }
 
   public function render() {
     if (!$this->mock) {
       throw new PhutilInvalidStateException('setMock');
     }
     $mock = $this->mock;
 
     $images_to_show = array();
     $thumbnail = null;
     if (!empty($this->images)) {
       $images_to_show = array_intersect_key(
         $this->mock->getImages(), array_flip($this->images));
     }
 
     $xform = PhabricatorFileTransform::getTransformByKey(
       PhabricatorFileThumbnailTransform::TRANSFORM_PINBOARD);
 
     if ($images_to_show) {
       $image = head($images_to_show);
       $thumbfile = $image->getFile();
       $header = 'M'.$mock->getID().' '.$mock->getName().
         ' (#'.$image->getID().')';
       $uri = '/M'.$this->mock->getID().'/'.$image->getID().'/';
     } else {
       $thumbfile = $mock->getCoverFile();
       $header = 'M'.$mock->getID().' '.$mock->getName();
       $uri = '/M'.$this->mock->getID();
     }
 
     $thumbnail = $thumbfile->getURIForTransform($xform);
     list($x, $y) = $xform->getTransformedDimensions($thumbfile);
 
     $item = id(new PHUIPinboardItemView())
+      ->setUser($this->getUser())
+      ->setObject($mock)
       ->setHeader($header)
       ->setURI($uri)
       ->setImageURI($thumbnail)
       ->setImageSize($x, $y)
       ->setDisabled($mock->isClosed())
       ->addIconCount('fa-picture-o', count($mock->getImages()))
       ->addIconCount('fa-trophy', $mock->getTokenCount());
 
     return $item;
   }
 
 }
diff --git a/src/applications/phortune/cart/PhortuneCartImplementation.php b/src/applications/phortune/cart/PhortuneCartImplementation.php
index 9085fe9aa..1dbc20080 100644
--- a/src/applications/phortune/cart/PhortuneCartImplementation.php
+++ b/src/applications/phortune/cart/PhortuneCartImplementation.php
@@ -1,46 +1,46 @@
 <?php
 
-abstract class PhortuneCartImplementation {
+abstract class PhortuneCartImplementation extends Phobject {
 
   /**
    * Load implementations for a given set of carts.
    *
    * Note that this method should return a map using the original keys to
    * identify which implementation corresponds to which cart.
    */
   abstract public function loadImplementationsForCarts(
     PhabricatorUser $viewer,
     array $carts);
 
   abstract public function getName(PhortuneCart $cart);
   abstract public function getCancelURI(PhortuneCart $cart);
   abstract public function getDoneURI(PhortuneCart $cart);
 
   public function getDescription(PhortuneCart $cart) {
     return null;
   }
 
   public function getDoneActionName(PhortuneCart $cart) {
     return pht('Return to Application');
   }
 
   public function assertCanCancelOrder(PhortuneCart $cart) {
     switch ($cart->getStatus()) {
       case PhortuneCart::STATUS_PURCHASED:
         throw new Exception(
           pht(
             'This order can not be cancelled because it has already been '.
             'completed.'));
         break;
     }
   }
 
   public function assertCanRefundOrder(PhortuneCart $cart) {
     return;
   }
 
   abstract public function willCreateCart(
     PhabricatorUser $viewer,
     PhortuneCart $cart);
 
 }
diff --git a/src/applications/phortune/constants/PhortuneConstants.php b/src/applications/phortune/constants/PhortuneConstants.php
index 2c11b1433..084e5899c 100644
--- a/src/applications/phortune/constants/PhortuneConstants.php
+++ b/src/applications/phortune/constants/PhortuneConstants.php
@@ -1,3 +1,3 @@
 <?php
 
-abstract class PhortuneConstants {}
+abstract class PhortuneConstants extends Phobject {}
diff --git a/src/applications/phortune/product/PhortuneProductImplementation.php b/src/applications/phortune/product/PhortuneProductImplementation.php
index 27f280ac5..84e6c6ee8 100644
--- a/src/applications/phortune/product/PhortuneProductImplementation.php
+++ b/src/applications/phortune/product/PhortuneProductImplementation.php
@@ -1,44 +1,44 @@
 <?php
 
-abstract class PhortuneProductImplementation {
+abstract class PhortuneProductImplementation extends Phobject {
 
   abstract public function loadImplementationsForRefs(
     PhabricatorUser $viewer,
     array $refs);
 
   abstract public function getRef();
   abstract public function getName(PhortuneProduct $product);
   abstract public function getPriceAsCurrency(PhortuneProduct $product);
 
   protected function getContentSource() {
     return PhabricatorContentSource::newForSource(
       PhabricatorContentSource::SOURCE_PHORTUNE,
       array());
   }
 
   public function getPurchaseName(
     PhortuneProduct $product,
     PhortunePurchase $purchase) {
     return $this->getName($product);
   }
 
   public function didPurchaseProduct(
     PhortuneProduct $product,
     PhortunePurchase $purchase) {
     return;
   }
 
   public function didRefundProduct(
     PhortuneProduct $product,
     PhortunePurchase $purchase,
     PhortuneCurrency $amount) {
     return;
   }
 
   public function getPurchaseURI(
     PhortuneProduct $product,
     PhortunePurchase $purchase) {
     return null;
   }
 
 }
diff --git a/src/applications/phortune/provider/PhortunePaymentProvider.php b/src/applications/phortune/provider/PhortunePaymentProvider.php
index da36779d0..97821a3e1 100644
--- a/src/applications/phortune/provider/PhortunePaymentProvider.php
+++ b/src/applications/phortune/provider/PhortunePaymentProvider.php
@@ -1,296 +1,296 @@
 <?php
 
 /**
  * @task addmethod  Adding Payment Methods
  */
-abstract class PhortunePaymentProvider {
+abstract class PhortunePaymentProvider extends Phobject {
 
   private $providerConfig;
 
   public function setProviderConfig(
     PhortunePaymentProviderConfig $provider_config) {
     $this->providerConfig = $provider_config;
     return $this;
   }
 
   public function getProviderConfig() {
     return $this->providerConfig;
   }
 
   /**
    * Return a short name which identifies this provider.
    */
   abstract public function getName();
 
 
 /* -(  Configuring Providers  )---------------------------------------------- */
 
 
   /**
    * Return a human-readable provider name for use on the merchant workflow
    * where a merchant owner adds providers.
    */
   abstract public function getConfigureName();
 
 
   /**
    * Return a human-readable provider description for use on the merchant
    * workflow where a merchant owner adds providers.
    */
   abstract public function getConfigureDescription();
 
   abstract public function getConfigureInstructions();
 
   abstract public function getConfigureProvidesDescription();
 
   abstract public function getAllConfigurableProperties();
 
   abstract public function getAllConfigurableSecretProperties();
   /**
    * Read a dictionary of properties from the provider's configuration for
    * use when editing the provider.
    */
   public function readEditFormValuesFromProviderConfig() {
     $properties = $this->getAllConfigurableProperties();
     $config = $this->getProviderConfig();
 
     $secrets = $this->getAllConfigurableSecretProperties();
     $secrets = array_fuse($secrets);
 
     $map = array();
     foreach ($properties as $property) {
       $map[$property] = $config->getMetadataValue($property);
       if (isset($secrets[$property])) {
         $map[$property] = $this->renderConfigurationSecret($map[$property]);
       }
     }
 
     return $map;
   }
 
 
   /**
    * Read a dictionary of properties from a request for use when editing the
    * provider.
    */
   public function readEditFormValuesFromRequest(AphrontRequest $request) {
     $properties = $this->getAllConfigurableProperties();
 
     $map = array();
     foreach ($properties as $property) {
       $map[$property] = $request->getStr($property);
     }
 
     return $map;
   }
 
 
   abstract public function processEditForm(
     AphrontRequest $request,
     array $values);
 
   abstract public function extendEditForm(
     AphrontRequest $request,
     AphrontFormView $form,
     array $values,
     array $issues);
 
   protected function renderConfigurationSecret($value) {
     if (strlen($value)) {
       return str_repeat('*', strlen($value));
     }
     return '';
   }
 
   public function isConfigurationSecret($value) {
     return preg_match('/^\*+\z/', trim($value));
   }
 
   abstract public function canRunConfigurationTest();
 
   public function runConfigurationTest() {
     throw new PhortuneNotImplementedException($this);
   }
 
 
 /* -(  Selecting Providers  )------------------------------------------------ */
 
 
   public static function getAllProviders() {
     return id(new PhutilSymbolLoader())
       ->setAncestorClass(__CLASS__)
       ->loadObjects();
   }
 
   public function isEnabled() {
     return $this->getProviderConfig()->getIsEnabled();
   }
 
   abstract public function isAcceptingLivePayments();
   abstract public function getPaymentMethodDescription();
   abstract public function getPaymentMethodIcon();
   abstract public function getPaymentMethodProviderDescription();
 
   final public function applyCharge(
     PhortunePaymentMethod $payment_method,
     PhortuneCharge $charge) {
     $this->executeCharge($payment_method, $charge);
   }
 
   final public function refundCharge(
     PhortuneCharge $charge,
     PhortuneCharge $refund) {
     $this->executeRefund($charge, $refund);
   }
 
   abstract protected function executeCharge(
     PhortunePaymentMethod $payment_method,
     PhortuneCharge $charge);
 
   abstract protected function executeRefund(
     PhortuneCharge $charge,
     PhortuneCharge $refund);
 
   abstract public function updateCharge(PhortuneCharge $charge);
 
 
 /* -(  Adding Payment Methods  )--------------------------------------------- */
 
 
   /**
    * @task addmethod
    */
   public function canCreatePaymentMethods() {
     return false;
   }
 
 
   /**
    * @task addmethod
    */
   public function translateCreatePaymentMethodErrorCode($error_code) {
     throw new PhortuneNotImplementedException($this);
   }
 
 
   /**
    * @task addmethod
    */
   public function getCreatePaymentMethodErrorMessage($error_code) {
     throw new PhortuneNotImplementedException($this);
   }
 
 
   /**
    * @task addmethod
    */
   public function validateCreatePaymentMethodToken(array $token) {
     throw new PhortuneNotImplementedException($this);
   }
 
 
   /**
    * @task addmethod
    */
   public function createPaymentMethodFromRequest(
     AphrontRequest $request,
     PhortunePaymentMethod $method,
     array $token) {
     throw new PhortuneNotImplementedException($this);
   }
 
 
   /**
    * @task addmethod
    */
   public function renderCreatePaymentMethodForm(
     AphrontRequest $request,
     array $errors) {
     throw new PhortuneNotImplementedException($this);
   }
 
   public function getDefaultPaymentMethodDisplayName(
     PhortunePaymentMethod $method) {
     throw new PhortuneNotImplementedException($this);
   }
 
 
 /* -(  One-Time Payments  )-------------------------------------------------- */
 
 
   public function canProcessOneTimePayments() {
     return false;
   }
 
   public function renderOneTimePaymentButton(
     PhortuneAccount $account,
     PhortuneCart $cart,
     PhabricatorUser $user) {
 
     require_celerity_resource('phortune-css');
 
     $description = $this->getPaymentMethodProviderDescription();
     $details = $this->getPaymentMethodDescription();
 
     $icon = id(new PHUIIconView())
       ->setSpriteSheet(PHUIIconView::SPRITE_LOGIN)
       ->setSpriteIcon($this->getPaymentMethodIcon());
 
     $button = id(new PHUIButtonView())
       ->setSize(PHUIButtonView::BIG)
       ->setColor(PHUIButtonView::GREY)
       ->setIcon($icon)
       ->setText($description)
       ->setSubtext($details);
 
     // NOTE: We generate a local URI to make sure the form picks up CSRF tokens.
     $uri = $this->getControllerURI(
       'checkout',
       array(
         'cartID' => $cart->getID(),
       ),
       $local = true);
 
     return phabricator_form(
       $user,
       array(
         'action' => $uri,
         'method' => 'POST',
       ),
       $button);
   }
 
 
 /* -(  Controllers  )-------------------------------------------------------- */
 
 
   final public function getControllerURI(
     $action,
     array $params = array(),
     $local = false) {
 
     $id = $this->getProviderConfig()->getID();
     $app = PhabricatorApplication::getByClass('PhabricatorPhortuneApplication');
     $path = $app->getBaseURI().'provider/'.$id.'/'.$action.'/';
 
     $uri = new PhutilURI($path);
     $uri->setQueryParams($params);
 
     if ($local) {
       return $uri;
     } else {
       return PhabricatorEnv::getURI((string)$uri);
     }
   }
 
   public function canRespondToControllerAction($action) {
     return false;
   }
 
   public function processControllerRequest(
     PhortuneProviderActionController $controller,
     AphrontRequest $request) {
     throw new PhortuneNotImplementedException($this);
   }
 
 }
diff --git a/src/applications/phortune/provider/__tests__/PhortunePaymentProviderTestCase.php b/src/applications/phortune/provider/__tests__/PhortunePaymentProviderTestCase.php
new file mode 100644
index 000000000..64ce50c55
--- /dev/null
+++ b/src/applications/phortune/provider/__tests__/PhortunePaymentProviderTestCase.php
@@ -0,0 +1,10 @@
+<?php
+
+final class PhortunePaymentProviderTestCase extends PhabricatorTestCase {
+
+  public function testGetAllProviders() {
+    PhortunePaymentProvider::getAllProviders();
+    $this->assertTrue(true);
+  }
+
+}
diff --git a/src/applications/phortune/subscription/PhortuneSubscriptionImplementation.php b/src/applications/phortune/subscription/PhortuneSubscriptionImplementation.php
index ad5126cf6..f8cbc2115 100644
--- a/src/applications/phortune/subscription/PhortuneSubscriptionImplementation.php
+++ b/src/applications/phortune/subscription/PhortuneSubscriptionImplementation.php
@@ -1,51 +1,51 @@
 <?php
 
-abstract class PhortuneSubscriptionImplementation {
+abstract class PhortuneSubscriptionImplementation extends Phobject {
 
   abstract public function loadImplementationsForRefs(
     PhabricatorUser $viewer,
     array $refs);
 
   abstract public function getRef();
   abstract public function getName(PhortuneSubscription $subscription);
 
   public function getFullName(PhortuneSubscription $subscription) {
     return $this->getName($subscription);
   }
 
   public function getCrumbName(PhortuneSubscription $subscription) {
     return $this->getName($subscription);
   }
 
   abstract public function getCostForBillingPeriodAsCurrency(
     PhortuneSubscription $subscription,
     $start_epoch,
     $end_epoch);
 
   public function shouldInvoiceForBillingPeriod(
     PhortuneSubscription $subscription,
     $start_epoch,
     $end_epoch) {
     return true;
   }
 
   public function getCartName(
     PhortuneSubscription $subscription,
     PhortuneCart $cart) {
     return pht('Subscription');
   }
 
   public function getPurchaseName(
     PhortuneSubscription $subscription,
     PhortuneProduct $product,
     PhortunePurchase $purchase) {
     return $product->getProductName();
   }
 
   public function getPurchaseURI(
     PhortuneSubscription $subscription,
     PhortuneProduct $product,
     PhortunePurchase $purchase) {
     return null;
   }
 }
diff --git a/src/applications/phortune/view/PhortuneCreditCardForm.php b/src/applications/phortune/view/PhortuneCreditCardForm.php
index 956e14499..6e4e2af38 100644
--- a/src/applications/phortune/view/PhortuneCreditCardForm.php
+++ b/src/applications/phortune/view/PhortuneCreditCardForm.php
@@ -1,122 +1,122 @@
 <?php
 
-final class PhortuneCreditCardForm {
+final class PhortuneCreditCardForm extends Phobject {
 
   private $formID;
   private $scripts = array();
   private $user;
   private $errors = array();
 
   private $cardNumberError;
   private $cardCVCError;
   private $cardExpirationError;
   private $securityAssurance;
 
   public function setSecurityAssurance($security_assurance) {
     $this->securityAssurance = $security_assurance;
     return $this;
   }
 
   public function getSecurityAssurance() {
     return $this->securityAssurance;
   }
 
   public function setUser(PhabricatorUser $user) {
     $this->user = $user;
     return $this;
   }
 
   public function setErrors(array $errors) {
     $this->errors = $errors;
     return $this;
   }
 
   public function addScript($script_uri) {
     $this->scripts[] = $script_uri;
     return $this;
   }
 
   public function getFormID() {
     if (!$this->formID) {
       $this->formID = celerity_generate_unique_node_id();
     }
     return $this->formID;
   }
 
   public function buildForm() {
     $form_id = $this->getFormID();
 
     require_celerity_resource('phortune-credit-card-form-css');
     require_celerity_resource('phortune-credit-card-form');
 
     require_celerity_resource('aphront-tooltip-css');
     Javelin::initBehavior('phabricator-tooltips');
 
     $form = new AphrontFormView();
 
     foreach ($this->scripts as $script) {
       $form->appendChild(
         phutil_tag(
           'script',
           array(
             'type' => 'text/javascript',
             'src'  => $script,
           )));
     }
 
     $errors = $this->errors;
     $e_number = isset($errors[PhortuneErrCode::ERR_CC_INVALID_NUMBER])
       ? pht('Invalid')
       : null;
 
     $e_cvc = isset($errors[PhortuneErrCode::ERR_CC_INVALID_CVC])
       ? pht('Invalid')
       : null;
 
     $e_expiry = isset($errors[PhortuneErrCode::ERR_CC_INVALID_EXPIRY])
       ? pht('Invalid')
       : null;
 
     $form
       ->setID($form_id)
       ->appendChild(
         id(new AphrontFormTextControl())
           ->setLabel(pht('Card Number'))
           ->setDisableAutocomplete(true)
           ->setSigil('number-input')
           ->setError($e_number))
       ->appendChild(
         id(new AphrontFormTextControl())
           ->setLabel(pht('CVC'))
           ->setDisableAutocomplete(true)
           ->addClass('aphront-form-cvc-input')
           ->setSigil('cvc-input')
           ->setError($e_cvc))
       ->appendChild(
         id(new PhortuneMonthYearExpiryControl())
           ->setLabel(pht('Expiration'))
           ->setUser($this->user)
           ->setError($e_expiry));
 
     $assurance = $this->getSecurityAssurance();
     if ($assurance) {
       $assurance = phutil_tag(
         'div',
         array(
           'class' => 'phortune-security-assurance',
         ),
         array(
           id(new PHUIIconView())
             ->setIconFont('fa-lock grey'),
           ' ',
           $assurance,
         ));
 
       $form->appendChild(
         id(new AphrontFormMarkupControl())
           ->setValue($assurance));
     }
 
     return $form;
   }
 }
diff --git a/src/applications/phriction/constants/PhrictionConstants.php b/src/applications/phriction/constants/PhrictionConstants.php
index 01ea5d8ec..9f6b1f6c6 100644
--- a/src/applications/phriction/constants/PhrictionConstants.php
+++ b/src/applications/phriction/constants/PhrictionConstants.php
@@ -1,3 +1,3 @@
 <?php
 
-abstract class PhrictionConstants {}
+abstract class PhrictionConstants extends Phobject {}
diff --git a/src/applications/policy/__tests__/PhabricatorPolicyDataTestCase.php b/src/applications/policy/__tests__/PhabricatorPolicyDataTestCase.php
index e9e9d21e0..f68eae2b4 100644
--- a/src/applications/policy/__tests__/PhabricatorPolicyDataTestCase.php
+++ b/src/applications/policy/__tests__/PhabricatorPolicyDataTestCase.php
@@ -1,153 +1,239 @@
 <?php
 
 final class PhabricatorPolicyDataTestCase extends PhabricatorTestCase {
 
   protected function getPhabricatorTestCaseConfiguration() {
     return array(
       self::PHABRICATOR_TESTCONFIG_BUILD_STORAGE_FIXTURES => true,
     );
   }
 
   public function testProjectPolicyMembership() {
     $author = $this->generateNewTestUser();
 
     $proj_a = id(new PhabricatorProject())
       ->setName('A')
       ->setAuthorPHID($author->getPHID())
       ->setIcon(PhabricatorProject::DEFAULT_ICON)
       ->setColor(PhabricatorProject::DEFAULT_COLOR)
       ->setIsMembershipLocked(0)
       ->save();
     $proj_b = id(new PhabricatorProject())
       ->setName('B')
       ->setAuthorPHID($author->getPHID())
       ->setIcon(PhabricatorProject::DEFAULT_ICON)
       ->setColor(PhabricatorProject::DEFAULT_COLOR)
       ->setIsMembershipLocked(0)
       ->save();
 
     $proj_a->setViewPolicy($proj_b->getPHID())->save();
     $proj_b->setViewPolicy($proj_a->getPHID())->save();
 
     $user = new PhabricatorUser();
 
     $results = id(new PhabricatorProjectQuery())
       ->setViewer($user)
       ->execute();
 
     $this->assertEqual(0, count($results));
   }
 
   public function testCustomPolicyRuleUser() {
     $user_a = $this->generateNewTestUser();
     $user_b = $this->generateNewTestUser();
     $author = $this->generateNewTestUser();
 
     $policy = id(new PhabricatorPolicy())
       ->setRules(
         array(
           array(
             'action' => PhabricatorPolicy::ACTION_ALLOW,
             'rule' => 'PhabricatorUsersPolicyRule',
             'value' => array($user_a->getPHID()),
           ),
         ))
       ->save();
 
     $task = ManiphestTask::initializeNewTask($author);
     $task->setViewPolicy($policy->getPHID());
     $task->save();
 
     $can_a_view = PhabricatorPolicyFilter::hasCapability(
       $user_a,
       $task,
       PhabricatorPolicyCapability::CAN_VIEW);
 
     $this->assertTrue($can_a_view);
 
     $can_b_view = PhabricatorPolicyFilter::hasCapability(
       $user_b,
       $task,
       PhabricatorPolicyCapability::CAN_VIEW);
 
     $this->assertFalse($can_b_view);
   }
 
   public function testCustomPolicyRuleAdministrators() {
     $user_a = $this->generateNewTestUser();
     $user_a->setIsAdmin(true)->save();
     $user_b = $this->generateNewTestUser();
     $author = $this->generateNewTestUser();
 
     $policy = id(new PhabricatorPolicy())
       ->setRules(
         array(
           array(
             'action' => PhabricatorPolicy::ACTION_ALLOW,
             'rule' => 'PhabricatorAdministratorsPolicyRule',
             'value' => null,
           ),
         ))
       ->save();
 
     $task = ManiphestTask::initializeNewTask($author);
     $task->setViewPolicy($policy->getPHID());
     $task->save();
 
     $can_a_view = PhabricatorPolicyFilter::hasCapability(
       $user_a,
       $task,
       PhabricatorPolicyCapability::CAN_VIEW);
 
     $this->assertTrue($can_a_view);
 
     $can_b_view = PhabricatorPolicyFilter::hasCapability(
       $user_b,
       $task,
       PhabricatorPolicyCapability::CAN_VIEW);
 
     $this->assertFalse($can_b_view);
   }
 
   public function testCustomPolicyRuleLunarPhase() {
     $user_a = $this->generateNewTestUser();
     $author = $this->generateNewTestUser();
 
     $policy = id(new PhabricatorPolicy())
       ->setRules(
         array(
           array(
             'action' => PhabricatorPolicy::ACTION_ALLOW,
             'rule' => 'PhabricatorLunarPhasePolicyRule',
             'value' => 'new',
           ),
         ))
       ->save();
 
     $task = ManiphestTask::initializeNewTask($author);
     $task->setViewPolicy($policy->getPHID());
     $task->save();
 
     $time_a = PhabricatorTime::pushTime(934354800, 'UTC');
 
       $can_a_view = PhabricatorPolicyFilter::hasCapability(
         $user_a,
         $task,
         PhabricatorPolicyCapability::CAN_VIEW);
       $this->assertTrue($can_a_view);
 
     unset($time_a);
 
 
     $time_b = PhabricatorTime::pushTime(1116745200, 'UTC');
 
       $can_a_view = PhabricatorPolicyFilter::hasCapability(
         $user_a,
         $task,
         PhabricatorPolicyCapability::CAN_VIEW);
       $this->assertFalse($can_a_view);
 
     unset($time_b);
   }
 
+  public function testObjectPolicyRuleTaskAuthor() {
+    $author = $this->generateNewTestUser();
+    $viewer = $this->generateNewTestUser();
+
+    $rule = new ManiphestTaskAuthorPolicyRule();
+
+    $task = ManiphestTask::initializeNewTask($author);
+    $task->setViewPolicy($rule->getObjectPolicyFullKey());
+    $task->save();
+
+    $this->assertTrue(
+      PhabricatorPolicyFilter::hasCapability(
+        $author,
+        $task,
+        PhabricatorPolicyCapability::CAN_VIEW));
+
+    $this->assertFalse(
+      PhabricatorPolicyFilter::hasCapability(
+        $viewer,
+        $task,
+        PhabricatorPolicyCapability::CAN_VIEW));
+  }
+
+  public function testObjectPolicyRuleThreadMembers() {
+    $author = $this->generateNewTestUser();
+    $viewer = $this->generateNewTestUser();
+
+    $rule = new ConpherenceThreadMembersPolicyRule();
+
+    $thread = ConpherenceThread::initializeNewRoom($author);
+    $thread->setViewPolicy($rule->getObjectPolicyFullKey());
+    $thread->save();
+
+    $this->assertFalse(
+      PhabricatorPolicyFilter::hasCapability(
+        $author,
+        $thread,
+        PhabricatorPolicyCapability::CAN_VIEW));
+
+    $this->assertFalse(
+      PhabricatorPolicyFilter::hasCapability(
+        $viewer,
+        $thread,
+        PhabricatorPolicyCapability::CAN_VIEW));
+
+    $participant = id(new ConpherenceParticipant())
+      ->setParticipantPHID($viewer->getPHID())
+      ->setConpherencePHID($thread->getPHID());
+
+    $thread->attachParticipants(array($viewer->getPHID() => $participant));
+
+    $this->assertTrue(
+      PhabricatorPolicyFilter::hasCapability(
+        $viewer,
+        $thread,
+        PhabricatorPolicyCapability::CAN_VIEW));
+  }
+
+  public function testObjectPolicyRuleSubscribers() {
+    $author = $this->generateNewTestUser();
+
+    $rule = new PhabricatorSubscriptionsSubscribersPolicyRule();
+
+    $task = ManiphestTask::initializeNewTask($author);
+    $task->setViewPolicy($rule->getObjectPolicyFullKey());
+    $task->save();
+
+    $this->assertFalse(
+      PhabricatorPolicyFilter::hasCapability(
+        $author,
+        $task,
+        PhabricatorPolicyCapability::CAN_VIEW));
+
+    id(new PhabricatorSubscriptionsEditor())
+      ->setActor($author)
+      ->setObject($task)
+      ->subscribeExplicit(array($author->getPHID()))
+      ->save();
+
+    $this->assertTrue(
+      PhabricatorPolicyFilter::hasCapability(
+        $author,
+        $task,
+        PhabricatorPolicyCapability::CAN_VIEW));
+  }
+
 }
diff --git a/src/applications/policy/__tests__/PhabricatorPolicyTestObject.php b/src/applications/policy/__tests__/PhabricatorPolicyTestObject.php
index 60c77aea8..0e8d4c495 100644
--- a/src/applications/policy/__tests__/PhabricatorPolicyTestObject.php
+++ b/src/applications/policy/__tests__/PhabricatorPolicyTestObject.php
@@ -1,67 +1,68 @@
 <?php
 
 /**
  * Configurable test object for implementing Policy unit tests.
  */
 final class PhabricatorPolicyTestObject
+  extends Phobject
   implements
     PhabricatorPolicyInterface,
     PhabricatorExtendedPolicyInterface {
 
   private $phid;
   private $capabilities = array();
   private $policies = array();
   private $automaticCapabilities = array();
   private $extendedPolicies = array();
 
   public function setPHID($phid) {
     $this->phid = $phid;
     return $this;
   }
 
   public function getPHID() {
     return $this->phid;
   }
 
   public function getCapabilities() {
     return $this->capabilities;
   }
 
   public function getPolicy($capability) {
     return idx($this->policies, $capability);
   }
 
   public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
     $auto = idx($this->automaticCapabilities, $capability, array());
     return idx($auto, $viewer->getPHID());
   }
 
   public function setCapabilities(array $capabilities) {
     $this->capabilities = $capabilities;
     return $this;
   }
 
   public function setPolicies(array $policy_map) {
     $this->policies = $policy_map;
     return $this;
   }
 
   public function setAutomaticCapabilities(array $auto_map) {
     $this->automaticCapabilities = $auto_map;
     return $this;
   }
 
   public function describeAutomaticCapability($capability) {
     return null;
   }
 
   public function setExtendedPolicies(array $extended_policies) {
     $this->extendedPolicies = $extended_policies;
     return $this;
   }
 
   public function getExtendedPolicy($capability, PhabricatorUser $viewer) {
     return idx($this->extendedPolicies, $capability, array());
   }
 
 }
diff --git a/src/applications/policy/application/PhabricatorPolicyApplication.php b/src/applications/policy/application/PhabricatorPolicyApplication.php
index cefccd2cb..8aaf8eb1b 100644
--- a/src/applications/policy/application/PhabricatorPolicyApplication.php
+++ b/src/applications/policy/application/PhabricatorPolicyApplication.php
@@ -1,27 +1,35 @@
 <?php
 
 final class PhabricatorPolicyApplication extends PhabricatorApplication {
 
   public function getName() {
     return pht('Policy');
   }
 
   public function isLaunchable() {
     return false;
   }
 
   public function canUninstall() {
     return false;
   }
 
   public function getRoutes() {
     return array(
       '/policy/' => array(
         'explain/(?P<phid>[^/]+)/(?P<capability>[^/]+)/'
           => 'PhabricatorPolicyExplainController',
-        'edit/(?:(?P<phid>[^/]+)/)?' => 'PhabricatorPolicyEditController',
+        'edit/'.
+          '(?:'.
+            'object/(?P<objectPHID>[^/]+)'.
+            '|'.
+            'type/(?P<objectType>[^/]+)'.
+            '|'.
+            'template/(?P<templateType>[^/]+)'.
+          ')/'.
+          '(?:(?P<phid>[^/]+)/)?' => 'PhabricatorPolicyEditController',
       ),
     );
   }
 
 }
diff --git a/src/applications/policy/capability/__tests__/PhabricatorPolicyCapabilityTestCase.php b/src/applications/policy/capability/__tests__/PhabricatorPolicyCapabilityTestCase.php
new file mode 100644
index 000000000..0def24762
--- /dev/null
+++ b/src/applications/policy/capability/__tests__/PhabricatorPolicyCapabilityTestCase.php
@@ -0,0 +1,11 @@
+<?php
+
+final class PhabricatorPolicyCapabilityTestCase
+  extends PhabricatorTestCase {
+
+  public function testGetCapabilityMap() {
+    PhabricatorPolicyCapability::getCapabilityMap();
+    $this->assertTrue(true);
+  }
+
+}
diff --git a/src/applications/policy/constants/PhabricatorPolicyConstants.php b/src/applications/policy/constants/PhabricatorPolicyConstants.php
index e9594c659..23e8236f3 100644
--- a/src/applications/policy/constants/PhabricatorPolicyConstants.php
+++ b/src/applications/policy/constants/PhabricatorPolicyConstants.php
@@ -1,3 +1,3 @@
 <?php
 
-abstract class PhabricatorPolicyConstants {}
+abstract class PhabricatorPolicyConstants extends Phobject {}
diff --git a/src/applications/policy/constants/PhabricatorPolicyType.php b/src/applications/policy/constants/PhabricatorPolicyType.php
index 51e8cb4c4..b5ce8f4b2 100644
--- a/src/applications/policy/constants/PhabricatorPolicyType.php
+++ b/src/applications/policy/constants/PhabricatorPolicyType.php
@@ -1,38 +1,42 @@
 <?php
 
 final class PhabricatorPolicyType extends PhabricatorPolicyConstants {
 
   const TYPE_GLOBAL       = 'global';
+  const TYPE_OBJECT       = 'object';
   const TYPE_USER         = 'user';
   const TYPE_CUSTOM       = 'custom';
   const TYPE_PROJECT      = 'project';
   const TYPE_MASKED       = 'masked';
 
   public static function getPolicyTypeOrder($type) {
     static $map = array(
       self::TYPE_GLOBAL   => 0,
-      self::TYPE_USER     => 1,
-      self::TYPE_CUSTOM   => 2,
-      self::TYPE_PROJECT  => 3,
+      self::TYPE_OBJECT   => 1,
+      self::TYPE_USER     => 2,
+      self::TYPE_CUSTOM   => 3,
+      self::TYPE_PROJECT  => 4,
       self::TYPE_MASKED   => 9,
     );
     return idx($map, $type, 9);
   }
 
   public static function getPolicyTypeName($type) {
     switch ($type) {
       case self::TYPE_GLOBAL:
         return pht('Basic Policies');
+      case self::TYPE_OBJECT:
+        return pht('Object Policies');
       case self::TYPE_USER:
         return pht('User Policies');
       case self::TYPE_CUSTOM:
         return pht('Advanced');
       case self::TYPE_PROJECT:
         return pht('Members of Project...');
       case self::TYPE_MASKED:
       default:
         return pht('Other Policies');
     }
   }
 
 }
diff --git a/src/applications/policy/controller/PhabricatorPolicyEditController.php b/src/applications/policy/controller/PhabricatorPolicyEditController.php
index 80b04e4ea..30c983655 100644
--- a/src/applications/policy/controller/PhabricatorPolicyEditController.php
+++ b/src/applications/policy/controller/PhabricatorPolicyEditController.php
@@ -1,224 +1,260 @@
 <?php
 
 final class PhabricatorPolicyEditController
   extends PhabricatorPolicyController {
 
   public function handleRequest(AphrontRequest $request) {
-    $request = $this->getRequest();
-    $viewer = $request->getUser();
+    $viewer = $this->getViewer();
+
+    // TODO: This doesn't do anything yet, but sets up template policies; see
+    // T6860.
+    $is_template = false;
+
+    $object_phid = $request->getURIData('objectPHID');
+    if ($object_phid) {
+      $object = id(new PhabricatorObjectQuery())
+        ->setViewer($viewer)
+        ->withPHIDs(array($object_phid))
+        ->executeOne();
+      if (!$object) {
+        return new Aphront404Response();
+      }
+    } else {
+      $object_type = $request->getURIData('objectType');
+      if (!$object_type) {
+        $object_type = $request->getURIData('templateType');
+        $is_template = true;
+      }
+
+      $phid_types = PhabricatorPHIDType::getAllInstalledTypes($viewer);
+      if (empty($phid_types[$object_type])) {
+        return new Aphront404Response();
+      }
+      $object = $phid_types[$object_type]->newObject();
+      if (!$object) {
+        return new Aphront404Response();
+      }
+    }
 
     $action_options = array(
       PhabricatorPolicy::ACTION_ALLOW => pht('Allow'),
       PhabricatorPolicy::ACTION_DENY => pht('Deny'),
     );
 
     $rules = id(new PhutilSymbolLoader())
       ->setAncestorClass('PhabricatorPolicyRule')
       ->loadObjects();
+
+    foreach ($rules as $key => $rule) {
+      if (!$rule->canApplyToObject($object)) {
+        unset($rules[$key]);
+      }
+    }
+
     $rules = msort($rules, 'getRuleOrder');
 
     $default_rule = array(
       'action' => head_key($action_options),
       'rule' => head_key($rules),
       'value' => null,
     );
 
     $phid = $request->getURIData('phid');
     if ($phid) {
       $policies = id(new PhabricatorPolicyQuery())
         ->setViewer($viewer)
         ->withPHIDs(array($phid))
         ->execute();
       if (!$policies) {
         return new Aphront404Response();
       }
       $policy = head($policies);
     } else {
       $policy = id(new PhabricatorPolicy())
         ->setRules(array($default_rule))
         ->setDefaultAction(PhabricatorPolicy::ACTION_DENY);
     }
 
     $root_id = celerity_generate_unique_node_id();
 
     $default_action = $policy->getDefaultAction();
     $rule_data = $policy->getRules();
 
     $errors = array();
     if ($request->isFormPost()) {
       $data = $request->getStr('rules');
       try {
         $data = phutil_json_decode($data);
       } catch (PhutilJSONParserException $ex) {
         throw new PhutilProxyException(
           pht('Failed to JSON decode rule data!'),
           $ex);
       }
 
       $rule_data = array();
       foreach ($data as $rule) {
         $action = idx($rule, 'action');
         switch ($action) {
           case 'allow':
           case 'deny':
             break;
           default:
             throw new Exception(pht("Invalid action '%s'!", $action));
         }
 
         $rule_class = idx($rule, 'rule');
         if (empty($rules[$rule_class])) {
           throw new Exception(pht("Invalid rule class '%s'!", $rule_class));
         }
 
         $rule_obj = $rules[$rule_class];
 
         $value = $rule_obj->getValueForStorage(idx($rule, 'value'));
 
         $rule_data[] = array(
           'action' => $action,
           'rule' => $rule_class,
           'value' => $value,
         );
       }
 
       // Filter out nonsense rules, like a "users" rule without any users
       // actually specified.
       $valid_rules = array();
       foreach ($rule_data as $rule) {
         $rule_class = $rule['rule'];
         if ($rules[$rule_class]->ruleHasEffect($rule['value'])) {
           $valid_rules[] = $rule;
         }
       }
 
       if (!$valid_rules) {
         $errors[] = pht('None of these policy rules have any effect.');
       }
 
       // NOTE: Policies are immutable once created, and we always create a new
       // policy here. If we didn't, we would need to lock this endpoint down,
       // as users could otherwise just go edit the policies of objects with
       // custom policies.
 
       if (!$errors) {
         $new_policy = new PhabricatorPolicy();
         $new_policy->setRules($valid_rules);
         $new_policy->setDefaultAction($request->getStr('default'));
         $new_policy->save();
 
         $data = array(
           'phid' => $new_policy->getPHID(),
           'info' => array(
             'name' => $new_policy->getName(),
             'full' => $new_policy->getName(),
             'icon' => $new_policy->getIcon(),
           ),
         );
 
         return id(new AphrontAjaxResponse())->setContent($data);
       }
     }
 
     // Convert rule values to display format (for example, expanding PHIDs
     // into tokens).
     foreach ($rule_data as $key => $rule) {
       $rule_data[$key]['value'] = $rules[$rule['rule']]->getValueForDisplay(
         $viewer,
         $rule['value']);
     }
 
     $default_select = AphrontFormSelectControl::renderSelectTag(
       $default_action,
       $action_options,
       array(
         'name' => 'default',
       ));
 
     if ($errors) {
       $errors = id(new PHUIInfoView())
         ->setErrors($errors);
     }
 
     $form = id(new PHUIFormLayoutView())
       ->appendChild($errors)
       ->appendChild(
         javelin_tag(
           'input',
           array(
             'type' => 'hidden',
             'name' => 'rules',
             'sigil' => 'rules',
           )))
       ->appendChild(
         id(new PHUIFormInsetView())
           ->setTitle(pht('Rules'))
           ->setRightButton(
             javelin_tag(
               'a',
               array(
                 'href' => '#',
                 'class' => 'button green',
                 'sigil' => 'create-rule',
                 'mustcapture' => true,
               ),
               pht('New Rule')))
           ->setDescription(pht('These rules are processed in order.'))
           ->setContent(javelin_tag(
             'table',
             array(
               'sigil' => 'rules',
               'class' => 'policy-rules-table',
             ),
             '')))
       ->appendChild(
         id(new AphrontFormMarkupControl())
           ->setLabel(pht('If No Rules Match'))
           ->setValue(pht(
             '%s all other users.',
             $default_select)));
 
     $form = phutil_tag(
       'div',
       array(
         'id' => $root_id,
       ),
       $form);
 
     $rule_options = mpull($rules, 'getRuleDescription');
     $type_map = mpull($rules, 'getValueControlType');
     $templates = mpull($rules, 'getValueControlTemplate');
 
     require_celerity_resource('policy-edit-css');
     Javelin::initBehavior(
       'policy-rule-editor',
       array(
         'rootID' => $root_id,
         'actions' => $action_options,
         'rules' => $rule_options,
         'types' => $type_map,
         'templates' => $templates,
         'data' => $rule_data,
         'defaultRule' => $default_rule,
       ));
 
     $title = pht('Custom Policy');
 
     $key = $request->getStr('capability');
     if ($key) {
       $capability = PhabricatorPolicyCapability::getCapabilityByKey($key);
       $title = pht('Custom "%s" Policy', $capability->getCapabilityName());
     }
 
     $dialog = id(new AphrontDialogView())
       ->setWidth(AphrontDialogView::WIDTH_FULL)
       ->setUser($viewer)
       ->setTitle($title)
       ->appendChild($form)
       ->addSubmitButton(pht('Save Policy'))
       ->addCancelButton('#');
 
     return id(new AphrontDialogResponse())->setDialog($dialog);
   }
 
 }
diff --git a/src/applications/policy/filter/PhabricatorPolicyFilter.php b/src/applications/policy/filter/PhabricatorPolicyFilter.php
index 46cb9352f..dcff36d33 100644
--- a/src/applications/policy/filter/PhabricatorPolicyFilter.php
+++ b/src/applications/policy/filter/PhabricatorPolicyFilter.php
@@ -1,747 +1,839 @@
 <?php
 
-final class PhabricatorPolicyFilter {
+final class PhabricatorPolicyFilter extends Phobject {
 
   private $viewer;
   private $objects;
   private $capabilities;
   private $raisePolicyExceptions;
   private $userProjects;
   private $customPolicies = array();
+  private $objectPolicies = array();
   private $forcedPolicy;
 
   public static function mustRetainCapability(
     PhabricatorUser $user,
     PhabricatorPolicyInterface $object,
     $capability) {
 
     if (!self::hasCapability($user, $object, $capability)) {
       throw new Exception(
         pht(
           "You can not make that edit, because it would remove your ability ".
           "to '%s' the object.",
           $capability));
     }
   }
 
   public static function requireCapability(
     PhabricatorUser $user,
     PhabricatorPolicyInterface $object,
     $capability) {
     $filter = id(new PhabricatorPolicyFilter())
       ->setViewer($user)
       ->requireCapabilities(array($capability))
       ->raisePolicyExceptions(true)
       ->apply(array($object));
   }
 
   /**
    * Perform a capability check, acting as though an object had a specific
    * policy. This is primarily used to check if a policy is valid (for example,
    * to prevent users from editing away their ability to edit an object).
    *
    * Specifically, a check like this:
    *
    *   PhabricatorPolicyFilter::requireCapabilityWithForcedPolicy(
    *     $viewer,
    *     $object,
    *     PhabricatorPolicyCapability::CAN_EDIT,
    *     $potential_new_policy);
    *
    * ...will throw a @{class:PhabricatorPolicyException} if the new policy would
    * remove the user's ability to edit the object.
    *
    * @param PhabricatorUser   The viewer to perform a policy check for.
    * @param PhabricatorPolicyInterface The object to perform a policy check on.
    * @param string            Capability to test.
    * @param string            Perform the test as though the object has this
    *                          policy instead of the policy it actually has.
    * @return void
    */
   public static function requireCapabilityWithForcedPolicy(
     PhabricatorUser $viewer,
     PhabricatorPolicyInterface $object,
     $capability,
     $forced_policy) {
 
     id(new PhabricatorPolicyFilter())
       ->setViewer($viewer)
       ->requireCapabilities(array($capability))
       ->raisePolicyExceptions(true)
       ->forcePolicy($forced_policy)
       ->apply(array($object));
   }
 
   public static function hasCapability(
     PhabricatorUser $user,
     PhabricatorPolicyInterface $object,
     $capability) {
 
     $filter = new PhabricatorPolicyFilter();
     $filter->setViewer($user);
     $filter->requireCapabilities(array($capability));
     $result = $filter->apply(array($object));
 
     return (count($result) == 1);
   }
 
   public function setViewer(PhabricatorUser $user) {
     $this->viewer = $user;
     return $this;
   }
 
   public function requireCapabilities(array $capabilities) {
     $this->capabilities = $capabilities;
     return $this;
   }
 
   public function raisePolicyExceptions($raise) {
     $this->raisePolicyExceptions = $raise;
     return $this;
   }
 
   public function forcePolicy($forced_policy) {
     $this->forcedPolicy = $forced_policy;
     return $this;
   }
 
   public function apply(array $objects) {
     assert_instances_of($objects, 'PhabricatorPolicyInterface');
 
     $viewer       = $this->viewer;
     $capabilities = $this->capabilities;
 
     if (!$viewer || !$capabilities) {
       throw new PhutilInvalidStateException('setViewer', 'requireCapabilities');
     }
 
     // If the viewer is omnipotent, short circuit all the checks and just
     // return the input unmodified. This is an optimization; we know the
     // result already.
     if ($viewer->isOmnipotent()) {
       return $objects;
     }
 
     $filtered = array();
     $viewer_phid = $viewer->getPHID();
 
     if (empty($this->userProjects[$viewer_phid])) {
       $this->userProjects[$viewer_phid] = array();
     }
 
     $need_projects = array();
     $need_policies = array();
+    $need_objpolicies = array();
     foreach ($objects as $key => $object) {
       $object_capabilities = $object->getCapabilities();
       foreach ($capabilities as $capability) {
         if (!in_array($capability, $object_capabilities)) {
           throw new Exception(
             pht(
               "Testing for capability '%s' on an object which does ".
               "not have that capability!",
               $capability));
         }
 
         $policy = $this->getObjectPolicy($object, $capability);
+
+        if (PhabricatorPolicyQuery::isObjectPolicy($policy)) {
+          $need_objpolicies[$policy][] = $object;
+          continue;
+        }
+
         $type = phid_get_type($policy);
         if ($type == PhabricatorProjectProjectPHIDType::TYPECONST) {
           $need_projects[$policy] = $policy;
+          continue;
         }
 
         if ($type == PhabricatorPolicyPHIDTypePolicy::TYPECONST) {
-          $need_policies[$policy] = $policy;
+          $need_policies[$policy][] = $object;
+          continue;
         }
       }
     }
 
+    if ($need_objpolicies) {
+      $this->loadObjectPolicies($need_objpolicies);
+    }
+
     if ($need_policies) {
-      $this->loadCustomPolicies(array_keys($need_policies));
+      $this->loadCustomPolicies($need_policies);
     }
 
     // If we need projects, check if any of the projects we need are also the
     // objects we're filtering. Because of how project rules work, this is a
     // common case.
     if ($need_projects) {
       foreach ($objects as $object) {
         if ($object instanceof PhabricatorProject) {
           $project_phid = $object->getPHID();
           if (isset($need_projects[$project_phid])) {
             $is_member = $object->isUserMember($viewer_phid);
             $this->userProjects[$viewer_phid][$project_phid] = $is_member;
             unset($need_projects[$project_phid]);
           }
         }
       }
     }
 
     if ($need_projects) {
       $need_projects = array_unique($need_projects);
 
       // NOTE: We're using the omnipotent user here to avoid a recursive
       // descent into madness. We don't actually need to know if the user can
       // see these projects or not, since: the check is "user is member of
       // project", not "user can see project"; and membership implies
       // visibility anyway. Without this, we may load other projects and
       // re-enter the policy filter and generally create a huge mess.
 
       $projects = id(new PhabricatorProjectQuery())
         ->setViewer(PhabricatorUser::getOmnipotentUser())
         ->withMemberPHIDs(array($viewer->getPHID()))
         ->withPHIDs($need_projects)
         ->execute();
 
       foreach ($projects as $project) {
         $this->userProjects[$viewer_phid][$project->getPHID()] = true;
       }
     }
 
     foreach ($objects as $key => $object) {
       foreach ($capabilities as $capability) {
         if (!$this->checkCapability($object, $capability)) {
           // If we're missing any capability, move on to the next object.
           continue 2;
         }
       }
 
       // If we make it here, we have all of the required capabilities.
       $filtered[$key] = $object;
     }
 
     // If we survied the primary checks, apply extended checks to objects
     // with extended policies.
     $results = array();
     $extended = array();
     foreach ($filtered as $key => $object) {
       if ($object instanceof PhabricatorExtendedPolicyInterface) {
         $extended[$key] = $object;
       } else {
         $results[$key] = $object;
       }
     }
 
     if ($extended) {
       $results += $this->applyExtendedPolicyChecks($extended);
       // Put results back in the original order.
       $results = array_select_keys($results, array_keys($filtered));
     }
 
     return $results;
   }
 
   private function applyExtendedPolicyChecks(array $extended_objects) {
     // First, we're going to detect cycles and reject any objects which are
     // part of a cycle. We don't want to loop forever if an object has a
     // self-referential or nonsense policy.
 
     static $in_flight = array();
 
     $all_phids = array();
     foreach ($extended_objects as $key => $object) {
       $phid = $object->getPHID();
       if (isset($in_flight[$phid])) {
         // TODO: This could be more user-friendly.
         $this->rejectObject($extended_objects[$key], false, '<cycle>');
         unset($extended_objects[$key]);
         continue;
       }
 
       // We might throw from rejectObject(), so we don't want to actually mark
       // anything as in-flight until we survive this entire step.
       $all_phids[$phid] = $phid;
     }
 
     foreach ($all_phids as $phid) {
       $in_flight[$phid] = true;
     }
 
     $caught = null;
     try {
       $extended_objects = $this->executeExtendedPolicyChecks($extended_objects);
     } catch (Exception $ex) {
       $caught = $ex;
     }
 
     foreach ($all_phids as $phid) {
       unset($in_flight[$phid]);
     }
 
     if ($caught) {
       throw $caught;
     }
 
     return $extended_objects;
   }
 
   private function executeExtendedPolicyChecks(array $extended_objects) {
     $viewer = $this->viewer;
     $filter_capabilities = $this->capabilities;
 
     // Iterate over the objects we need to filter and pull all the nonempty
     // policies into a flat, structured list.
     $all_structs = array();
     foreach ($extended_objects as $key => $extended_object) {
       foreach ($filter_capabilities as $extended_capability) {
         $extended_policies = $extended_object->getExtendedPolicy(
           $extended_capability,
           $viewer);
         if (!$extended_policies) {
           continue;
         }
 
         foreach ($extended_policies as $extended_policy) {
           list($object, $capabilities) = $extended_policy;
 
           // Build a description of the capabilities we need to check. This
           // will be something like `"view"`, or `"edit view"`, or possibly
           // a longer string with custom capabilities. Later, group the objects
           // up into groups which need the same capabilities tested.
           $capabilities = (array)$capabilities;
           $capabilities = array_fuse($capabilities);
           ksort($capabilities);
           $group = implode(' ', $capabilities);
 
           $struct = array(
             'key' => $key,
             'for' => $extended_capability,
             'object' => $object,
             'capabilities' => $capabilities,
             'group' => $group,
           );
 
           $all_structs[] = $struct;
         }
       }
     }
 
     // Extract any bare PHIDs from the structs; we need to load these objects.
     // These are objects which are required in order to perform an extended
     // policy check but which the original viewer did not have permission to
     // see (they presumably had other permissions which let them load the
     // object in the first place).
     $all_phids = array();
     foreach ($all_structs as $idx => $struct) {
       $object = $struct['object'];
       if (is_string($object)) {
         $all_phids[$object] = $object;
       }
     }
 
     // If we have some bare PHIDs, we need to load the corresponding objects.
     if ($all_phids) {
       // We can pull these with the omnipotent user because we're immediately
       // filtering them.
       $ref_objects = id(new PhabricatorObjectQuery())
         ->setViewer(PhabricatorUser::getOmnipotentUser())
         ->withPHIDs($all_phids)
         ->execute();
       $ref_objects = mpull($ref_objects, null, 'getPHID');
     } else {
       $ref_objects = array();
     }
 
     // Group the list of checks by the capabilities we need to check.
     $groups = igroup($all_structs, 'group');
     foreach ($groups as $structs) {
       $head = head($structs);
 
       // All of the items in each group are checking for the same capabilities.
       $capabilities = $head['capabilities'];
 
       $key_map = array();
       $objects_in = array();
       foreach ($structs as $struct) {
         $extended_key = $struct['key'];
         if (empty($extended_objects[$key])) {
           // If this object has already been rejected by an earlier filtering
           // pass, we don't need to do any tests on it.
           continue;
         }
 
         $object = $struct['object'];
         if (is_string($object)) {
           // This is really a PHID, so look it up.
           $object_phid = $object;
           if (empty($ref_objects[$object_phid])) {
             // We weren't able to load the corresponding object, so just
             // reject this result outright.
 
             $reject = $extended_objects[$key];
             unset($extended_objects[$key]);
 
             // TODO: This could be friendlier.
             $this->rejectObject($reject, false, '<bad-ref>');
             continue;
           }
           $object = $ref_objects[$object_phid];
         }
 
         $phid = $object->getPHID();
 
         $key_map[$phid][] = $extended_key;
         $objects_in[$phid] = $object;
       }
 
       if ($objects_in) {
         $objects_out = id(new PhabricatorPolicyFilter())
           ->setViewer($viewer)
           ->requireCapabilities($capabilities)
           ->apply($objects_in);
         $objects_out = mpull($objects_out, null, 'getPHID');
       } else {
         $objects_out = array();
       }
 
       // If any objects were removed by filtering, we're going to reject all
       // of the original objects which needed them.
       foreach ($objects_in as $phid => $object_in) {
         if (isset($objects_out[$phid])) {
           // This object survived filtering, so we don't need to throw any
           // results away.
           continue;
         }
 
         foreach ($key_map[$phid] as $extended_key) {
           if (empty($extended_objects[$extended_key])) {
             // We've already rejected this object, so we don't need to reject
             // it again.
             continue;
           }
 
           $reject = $extended_objects[$extended_key];
           unset($extended_objects[$extended_key]);
 
           // TODO: This isn't as user-friendly as it could be. It's possible
           // that we're rejecting this object for multiple capability/policy
           // failures, though.
           $this->rejectObject($reject, false, '<extended>');
         }
       }
     }
 
     return $extended_objects;
   }
 
   private function checkCapability(
     PhabricatorPolicyInterface $object,
     $capability) {
 
     $policy = $this->getObjectPolicy($object, $capability);
 
     if (!$policy) {
       // TODO: Formalize this somehow?
       $policy = PhabricatorPolicies::POLICY_USER;
     }
 
     if ($policy == PhabricatorPolicies::POLICY_PUBLIC) {
       // If the object is set to "public" but that policy is disabled for this
       // install, restrict the policy to "user".
       if (!PhabricatorEnv::getEnvConfig('policy.allow-public')) {
         $policy = PhabricatorPolicies::POLICY_USER;
       }
 
       // If the object is set to "public" but the capability is not a public
       // capability, restrict the policy to "user".
       $capobj = PhabricatorPolicyCapability::getCapabilityByKey($capability);
       if (!$capobj || !$capobj->shouldAllowPublicPolicySetting()) {
         $policy = PhabricatorPolicies::POLICY_USER;
       }
     }
 
     $viewer = $this->viewer;
 
     if ($viewer->isOmnipotent()) {
       return true;
     }
 
     if ($object instanceof PhabricatorSpacesInterface) {
       $space_phid = $object->getSpacePHID();
       if (!$this->canViewerSeeObjectsInSpace($viewer, $space_phid)) {
         $this->rejectObjectFromSpace($object, $space_phid);
         return false;
       }
     }
 
     if ($object->hasAutomaticCapability($capability, $viewer)) {
       return true;
     }
 
     switch ($policy) {
       case PhabricatorPolicies::POLICY_PUBLIC:
         return true;
       case PhabricatorPolicies::POLICY_USER:
         if ($viewer->getPHID()) {
           return true;
         } else {
           $this->rejectObject($object, $policy, $capability);
         }
         break;
       case PhabricatorPolicies::POLICY_ADMIN:
         if ($viewer->getIsAdmin()) {
           return true;
         } else {
           $this->rejectObject($object, $policy, $capability);
         }
         break;
       case PhabricatorPolicies::POLICY_NOONE:
         $this->rejectObject($object, $policy, $capability);
         break;
       default:
+        if (PhabricatorPolicyQuery::isObjectPolicy($policy)) {
+          if ($this->checkObjectPolicy($policy, $object)) {
+            return true;
+          } else {
+            $this->rejectObject($object, $policy, $capability);
+            break;
+          }
+        }
+
         $type = phid_get_type($policy);
         if ($type == PhabricatorProjectProjectPHIDType::TYPECONST) {
           if (!empty($this->userProjects[$viewer->getPHID()][$policy])) {
             return true;
           } else {
             $this->rejectObject($object, $policy, $capability);
           }
         } else if ($type == PhabricatorPeopleUserPHIDType::TYPECONST) {
           if ($viewer->getPHID() == $policy) {
             return true;
           } else {
             $this->rejectObject($object, $policy, $capability);
           }
         } else if ($type == PhabricatorPolicyPHIDTypePolicy::TYPECONST) {
-          if ($this->checkCustomPolicy($policy)) {
+          if ($this->checkCustomPolicy($policy, $object)) {
             return true;
           } else {
             $this->rejectObject($object, $policy, $capability);
           }
         } else {
           // Reject objects with unknown policies.
           $this->rejectObject($object, false, $capability);
         }
     }
 
     return false;
   }
 
   public function rejectObject(
     PhabricatorPolicyInterface $object,
     $policy,
     $capability) {
 
     if (!$this->raisePolicyExceptions) {
       return;
     }
 
     if ($this->viewer->isOmnipotent()) {
       // Never raise policy exceptions for the omnipotent viewer. Although we
       // will never normally issue a policy rejection for the omnipotent
       // viewer, we can end up here when queries blanket reject objects that
       // have failed to load, without distinguishing between nonexistent and
       // nonvisible objects.
       return;
     }
 
     $capobj = PhabricatorPolicyCapability::getCapabilityByKey($capability);
     $rejection = null;
     if ($capobj) {
       $rejection = $capobj->describeCapabilityRejection();
       $capability_name = $capobj->getCapabilityName();
     } else {
       $capability_name = $capability;
     }
 
     if (!$rejection) {
       // We couldn't find the capability object, or it doesn't provide a
       // tailored rejection string.
       $rejection = pht(
         'You do not have the required capability ("%s") to do whatever you '.
         'are trying to do.',
         $capability);
     }
 
     $more = PhabricatorPolicy::getPolicyExplanation($this->viewer, $policy);
     $exceptions = $object->describeAutomaticCapability($capability);
 
     $details = array_filter(array_merge(array($more), (array)$exceptions));
 
     $access_denied = $this->renderAccessDenied($object);
 
     $full_message = pht(
       '[%s] (%s) %s // %s',
       $access_denied,
       $capability_name,
       $rejection,
       implode(' ', $details));
 
     $exception = id(new PhabricatorPolicyException($full_message))
       ->setTitle($access_denied)
       ->setRejection($rejection)
       ->setCapabilityName($capability_name)
       ->setMoreInfo($details);
 
     throw $exception;
   }
 
-  private function loadCustomPolicies(array $phids) {
+  private function loadObjectPolicies(array $map) {
+    $viewer = $this->viewer;
+    $viewer_phid = $viewer->getPHID();
+
+    $rules = PhabricatorPolicyQuery::getObjectPolicyRules(null);
+
+    $results = array();
+    foreach ($map as $key => $object_list) {
+      $rule = idx($rules, $key);
+      if (!$rule) {
+        continue;
+      }
+
+      foreach ($object_list as $object_key => $object) {
+        if (!$rule->canApplyToObject($object)) {
+          unset($object_list[$object_key]);
+        }
+      }
+
+      $rule->willApplyRules($viewer, array(), $object_list);
+      $results[$key] = $rule;
+    }
+
+    $this->objectPolicies[$viewer_phid] = $results;
+  }
+
+  private function loadCustomPolicies(array $map) {
     $viewer = $this->viewer;
     $viewer_phid = $viewer->getPHID();
 
     $custom_policies = id(new PhabricatorPolicyQuery())
       ->setViewer($viewer)
-      ->withPHIDs($phids)
+      ->withPHIDs(array_keys($map))
       ->execute();
     $custom_policies = mpull($custom_policies, null, 'getPHID');
 
-
     $classes = array();
     $values = array();
-    foreach ($custom_policies as $policy) {
+    $objects = array();
+    foreach ($custom_policies as $policy_phid => $policy) {
       foreach ($policy->getCustomRuleClasses() as $class) {
         $classes[$class] = $class;
         $values[$class][] = $policy->getCustomRuleValues($class);
+
+        foreach (idx($map, $policy_phid, array()) as $object) {
+          $objects[$class][] = $object;
+        }
       }
     }
 
     foreach ($classes as $class => $ignored) {
-      $object = newv($class, array());
-      $object->willApplyRules($viewer, array_mergev($values[$class]));
-      $classes[$class] = $object;
+      $rule_object = newv($class, array());
+
+      // Filter out any objects which the rule can't apply to.
+      $target_objects = idx($objects, $class, array());
+      foreach ($target_objects as $key => $target_object) {
+        if (!$rule_object->canApplyToObject($target_object)) {
+          unset($target_objects[$key]);
+        }
+      }
+
+      $rule_object->willApplyRules(
+        $viewer,
+        array_mergev($values[$class]),
+        $target_objects);
+
+      $classes[$class] = $rule_object;
     }
 
     foreach ($custom_policies as $policy) {
       $policy->attachRuleObjects($classes);
     }
 
     if (empty($this->customPolicies[$viewer_phid])) {
       $this->customPolicies[$viewer_phid] = array();
     }
 
     $this->customPolicies[$viewer->getPHID()] += $custom_policies;
   }
 
-  private function checkCustomPolicy($policy_phid) {
+  private function checkObjectPolicy(
+    $policy_phid,
+    PhabricatorPolicyInterface $object) {
+    $viewer = $this->viewer;
+    $viewer_phid = $viewer->getPHID();
+
+    $rule = idx($this->objectPolicies[$viewer_phid], $policy_phid);
+    if (!$rule) {
+      return false;
+    }
+
+    if (!$rule->canApplyToObject($object)) {
+      return false;
+    }
+
+    return $rule->applyRule($viewer, null, $object);
+  }
+
+  private function checkCustomPolicy(
+    $policy_phid,
+    PhabricatorPolicyInterface $object) {
+
     $viewer = $this->viewer;
     $viewer_phid = $viewer->getPHID();
 
     $policy = idx($this->customPolicies[$viewer_phid], $policy_phid);
     if (!$policy) {
       // Reject, this policy is bogus.
       return false;
     }
 
     $objects = $policy->getRuleObjects();
     $action = null;
     foreach ($policy->getRules() as $rule) {
-      $object = idx($objects, idx($rule, 'rule'));
-      if (!$object) {
+      $rule_object = idx($objects, idx($rule, 'rule'));
+      if (!$rule_object) {
         // Reject, this policy has a bogus rule.
         return false;
       }
 
+      if (!$rule_object->canApplyToObject($object)) {
+        // Reject, this policy rule can't be applied to the given object.
+        return false;
+      }
+
       // If the user matches this rule, use this action.
-      if ($object->applyRule($viewer, idx($rule, 'value'))) {
+      if ($rule_object->applyRule($viewer, idx($rule, 'value'), $object)) {
         $action = idx($rule, 'action');
         break;
       }
     }
 
     if ($action === null) {
       $action = $policy->getDefaultAction();
     }
 
     if ($action === PhabricatorPolicy::ACTION_ALLOW) {
       return true;
     }
 
     return false;
   }
 
   private function getObjectPolicy(
     PhabricatorPolicyInterface $object,
     $capability) {
 
     if ($this->forcedPolicy) {
       return $this->forcedPolicy;
     } else {
       return $object->getPolicy($capability);
     }
   }
 
   private function renderAccessDenied(PhabricatorPolicyInterface $object) {
     // NOTE: Not every type of policy object has a real PHID; just load an
     // empty handle if a real PHID isn't available.
     $phid = nonempty($object->getPHID(), PhabricatorPHIDConstants::PHID_VOID);
 
     $handle = id(new PhabricatorHandleQuery())
       ->setViewer($this->viewer)
       ->withPHIDs(array($phid))
       ->executeOne();
 
     $object_name = $handle->getObjectName();
 
     $is_serious = PhabricatorEnv::getEnvConfig('phabricator.serious-business');
     if ($is_serious) {
       $access_denied = pht(
         'Access Denied: %s',
         $object_name);
     } else {
       $access_denied = pht(
         'You Shall Not Pass: %s',
         $object_name);
     }
 
     return $access_denied;
   }
 
 
   private function canViewerSeeObjectsInSpace(
     PhabricatorUser $viewer,
     $space_phid) {
 
     $spaces = PhabricatorSpacesNamespaceQuery::getAllSpaces();
 
     // If there are no spaces, everything exists in an implicit default space
     // with no policy controls. This is the default state.
     if (!$spaces) {
       if ($space_phid !== null) {
         return false;
       } else {
         return true;
       }
     }
 
     if ($space_phid === null) {
       $space = PhabricatorSpacesNamespaceQuery::getDefaultSpace();
     } else {
       $space = idx($spaces, $space_phid);
     }
 
     if (!$space) {
       return false;
     }
 
     // This may be more involved later, but for now being able to see the
     // space is equivalent to being able to see everything in it.
     return self::hasCapability(
       $viewer,
       $space,
       PhabricatorPolicyCapability::CAN_VIEW);
   }
 
   private function rejectObjectFromSpace(
     PhabricatorPolicyInterface $object,
     $space_phid) {
 
     if (!$this->raisePolicyExceptions) {
       return;
     }
 
     if ($this->viewer->isOmnipotent()) {
       return;
     }
 
     $access_denied = $this->renderAccessDenied($object);
 
     $rejection = pht(
       'This object is in a space you do not have permission to access.');
     $full_message = pht('[%s] %s', $access_denied, $rejection);
 
     $exception = id(new PhabricatorPolicyException($full_message))
       ->setTitle($access_denied)
       ->setRejection($rejection);
 
     throw $exception;
   }
 
 }
diff --git a/src/applications/policy/query/PhabricatorPolicyQuery.php b/src/applications/policy/query/PhabricatorPolicyQuery.php
index 6a5c2219d..496c704b7 100644
--- a/src/applications/policy/query/PhabricatorPolicyQuery.php
+++ b/src/applications/policy/query/PhabricatorPolicyQuery.php
@@ -1,237 +1,346 @@
 <?php
 
 final class PhabricatorPolicyQuery
   extends PhabricatorCursorPagedPolicyAwareQuery {
 
   private $object;
   private $phids;
 
+  const OBJECT_POLICY_PREFIX = 'obj.';
+
   public function setObject(PhabricatorPolicyInterface $object) {
     $this->object = $object;
     return $this;
   }
 
   public function withPHIDs(array $phids) {
     $this->phids = $phids;
     return $this;
   }
 
   public static function loadPolicies(
     PhabricatorUser $viewer,
     PhabricatorPolicyInterface $object) {
 
     $results = array();
 
     $map = array();
     foreach ($object->getCapabilities() as $capability) {
       $map[$capability] = $object->getPolicy($capability);
     }
 
     $policies = id(new PhabricatorPolicyQuery())
       ->setViewer($viewer)
       ->withPHIDs($map)
       ->execute();
 
     foreach ($map as $capability => $phid) {
       $results[$capability] = $policies[$phid];
     }
 
     return $results;
   }
 
   public static function renderPolicyDescriptions(
     PhabricatorUser $viewer,
     PhabricatorPolicyInterface $object,
     $icon = false) {
 
     $policies = self::loadPolicies($viewer, $object);
 
     foreach ($policies as $capability => $policy) {
       $policies[$capability] = $policy->renderDescription($icon);
     }
 
     return $policies;
   }
 
   protected function loadPage() {
     if ($this->object && $this->phids) {
       throw new Exception(
         pht(
           'You can not issue a policy query with both %s and %s.',
           'setObject()',
           'setPHIDs()'));
     } else if ($this->object) {
       $phids = $this->loadObjectPolicyPHIDs();
     } else {
       $phids = $this->phids;
     }
 
     $phids = array_fuse($phids);
 
     $results = array();
 
     // First, load global policies.
-    foreach ($this->getGlobalPolicies() as $phid => $policy) {
+    foreach (self::getGlobalPolicies() as $phid => $policy) {
+      if (isset($phids[$phid])) {
+        $results[$phid] = $policy;
+        unset($phids[$phid]);
+      }
+    }
+
+    // Now, load object policies.
+    foreach (self::getObjectPolicies($this->object) as $phid => $policy) {
       if (isset($phids[$phid])) {
         $results[$phid] = $policy;
         unset($phids[$phid]);
       }
     }
 
     // If we still need policies, we're going to have to fetch data. Bucket
     // the remaining policies into rule-based policies and handle-based
     // policies.
     if ($phids) {
       $rule_policies = array();
       $handle_policies = array();
       foreach ($phids as $phid) {
         $phid_type = phid_get_type($phid);
         if ($phid_type == PhabricatorPolicyPHIDTypePolicy::TYPECONST) {
           $rule_policies[$phid] = $phid;
         } else {
           $handle_policies[$phid] = $phid;
         }
       }
 
       if ($handle_policies) {
         $handles = id(new PhabricatorHandleQuery())
           ->setViewer($this->getViewer())
           ->withPHIDs($handle_policies)
           ->execute();
         foreach ($handle_policies as $phid) {
           $results[$phid] = PhabricatorPolicy::newFromPolicyAndHandle(
             $phid,
             $handles[$phid]);
         }
       }
 
       if ($rule_policies) {
         $rules = id(new PhabricatorPolicy())->loadAllWhere(
           'phid IN (%Ls)',
           $rule_policies);
         $results += mpull($rules, null, 'getPHID');
       }
     }
 
     $results = msort($results, 'getSortKey');
 
     return $results;
   }
 
   public static function isGlobalPolicy($policy) {
     $global_policies = self::getGlobalPolicies();
 
     if (isset($global_policies[$policy])) {
       return true;
     }
 
     return false;
   }
 
   public static function getGlobalPolicy($policy) {
     if (!self::isGlobalPolicy($policy)) {
       throw new Exception(pht("Policy '%s' is not a global policy!", $policy));
     }
     return idx(self::getGlobalPolicies(), $policy);
   }
 
   private static function getGlobalPolicies() {
     static $constants = array(
       PhabricatorPolicies::POLICY_PUBLIC,
       PhabricatorPolicies::POLICY_USER,
       PhabricatorPolicies::POLICY_ADMIN,
       PhabricatorPolicies::POLICY_NOONE,
     );
 
     $results = array();
     foreach ($constants as $constant) {
       $results[$constant] = id(new PhabricatorPolicy())
         ->setType(PhabricatorPolicyType::TYPE_GLOBAL)
         ->setPHID($constant)
         ->setName(self::getGlobalPolicyName($constant))
         ->setShortName(self::getGlobalPolicyShortName($constant))
         ->makeEphemeral();
     }
 
     return $results;
   }
 
   private static function getGlobalPolicyName($policy) {
     switch ($policy) {
       case PhabricatorPolicies::POLICY_PUBLIC:
         return pht('Public (No Login Required)');
       case PhabricatorPolicies::POLICY_USER:
         return pht('All Users');
       case PhabricatorPolicies::POLICY_ADMIN:
         return pht('Administrators');
       case PhabricatorPolicies::POLICY_NOONE:
         return pht('No One');
       default:
         return pht('Unknown Policy');
     }
   }
 
   private static function getGlobalPolicyShortName($policy) {
     switch ($policy) {
       case PhabricatorPolicies::POLICY_PUBLIC:
         return pht('Public');
       default:
         return null;
     }
   }
 
   private function loadObjectPolicyPHIDs() {
     $phids = array();
     $viewer = $this->getViewer();
 
     if ($viewer->getPHID()) {
       $projects = id(new PhabricatorProjectQuery())
         ->setViewer($viewer)
         ->withMemberPHIDs(array($viewer->getPHID()))
         ->execute();
       foreach ($projects as $project) {
         $phids[] = $project->getPHID();
       }
 
       // Include the "current viewer" policy. This improves consistency, but
       // is also useful for creating private instances of normally-shared object
       // types, like repositories.
       $phids[] = $viewer->getPHID();
     }
 
     $capabilities = $this->object->getCapabilities();
     foreach ($capabilities as $capability) {
       $policy = $this->object->getPolicy($capability);
       if (!$policy) {
         continue;
       }
       $phids[] = $policy;
     }
 
     // If this install doesn't have "Public" enabled, don't include it as an
     // option unless the object already has a "Public" policy. In this case we
     // retain the policy but enforce it as though it was "All Users".
     $show_public = PhabricatorEnv::getEnvConfig('policy.allow-public');
-    foreach ($this->getGlobalPolicies() as $phid => $policy) {
+    foreach (self::getGlobalPolicies() as $phid => $policy) {
       if ($phid == PhabricatorPolicies::POLICY_PUBLIC) {
         if (!$show_public) {
           continue;
         }
       }
       $phids[] = $phid;
     }
 
+    foreach (self::getObjectPolicies($this->object) as $phid => $policy) {
+      $phids[] = $phid;
+    }
+
     return $phids;
   }
 
   protected function shouldDisablePolicyFiltering() {
     // Policy filtering of policies is currently perilous and not required by
     // the application.
     return true;
   }
 
   public function getQueryApplicationClass() {
     return 'PhabricatorPolicyApplication';
   }
 
+  public static function isSpecialPolicy($identifier) {
+    if (self::isObjectPolicy($identifier)) {
+      return true;
+    }
+
+    if (self::isGlobalPolicy($identifier)) {
+      return true;
+    }
+
+    return false;
+  }
+
+
+/* -(  Object Policies  )---------------------------------------------------- */
+
+
+  public static function isObjectPolicy($identifier) {
+    $prefix = self::OBJECT_POLICY_PREFIX;
+    return !strncmp($identifier, $prefix, strlen($prefix));
+  }
+
+  public static function getObjectPolicy($identifier) {
+    if (!self::isObjectPolicy($identifier)) {
+      return null;
+    }
+
+    $policies = self::getObjectPolicies(null);
+    return idx($policies, $identifier);
+  }
+
+  public static function getObjectPolicyRule($identifier) {
+    if (!self::isObjectPolicy($identifier)) {
+      return null;
+    }
+
+    $rules = self::getObjectPolicyRules(null);
+    return idx($rules, $identifier);
+  }
+
+  public static function getObjectPolicies($object) {
+    $rule_map = self::getObjectPolicyRules($object);
+
+    $results = array();
+    foreach ($rule_map as $key => $rule) {
+      $results[$key] = id(new PhabricatorPolicy())
+        ->setType(PhabricatorPolicyType::TYPE_OBJECT)
+        ->setPHID($key)
+        ->setIcon($rule->getObjectPolicyIcon())
+        ->setName($rule->getObjectPolicyName())
+        ->setShortName($rule->getObjectPolicyShortName())
+        ->makeEphemeral();
+    }
+
+    return $results;
+  }
+
+  public static function getObjectPolicyRules($object) {
+    $rules = id(new PhutilSymbolLoader())
+      ->setAncestorClass('PhabricatorPolicyRule')
+      ->loadObjects();
+
+    $results = array();
+    foreach ($rules as $rule) {
+      $key = $rule->getObjectPolicyKey();
+      if (!$key) {
+        continue;
+      }
+
+      $full_key = $rule->getObjectPolicyFullKey();
+      if (isset($results[$full_key])) {
+        throw new Exception(
+          pht(
+            'Two policy rules (of classes "%s" and "%s") define the same '.
+            'object policy key ("%s"), but each object policy rule must use '.
+            'a unique key.',
+            get_class($rule),
+            get_class($results[$full_key]),
+            $key));
+      }
+
+      $results[$full_key] = $rule;
+    }
+
+    if ($object !== null) {
+      foreach ($results as $key => $rule) {
+        if (!$rule->canApplyToObject($object)) {
+          unset($results[$key]);
+        }
+      }
+    }
+
+    return $results;
+  }
+
+
 }
diff --git a/src/applications/policy/rule/PhabricatorAdministratorsPolicyRule.php b/src/applications/policy/rule/PhabricatorAdministratorsPolicyRule.php
index f232cb300..b6e450add 100644
--- a/src/applications/policy/rule/PhabricatorAdministratorsPolicyRule.php
+++ b/src/applications/policy/rule/PhabricatorAdministratorsPolicyRule.php
@@ -1,17 +1,20 @@
 <?php
 
 final class PhabricatorAdministratorsPolicyRule extends PhabricatorPolicyRule {
 
   public function getRuleDescription() {
     return pht('administrators');
   }
 
-  public function applyRule(PhabricatorUser $viewer, $value) {
+  public function applyRule(
+    PhabricatorUser $viewer,
+    $value,
+    PhabricatorPolicyInterface $object) {
     return $viewer->getIsAdmin();
   }
 
   public function getValueControlType() {
     return self::CONTROL_TYPE_NONE;
   }
 
 }
diff --git a/src/applications/policy/rule/PhabricatorLegalpadSignaturePolicyRule.php b/src/applications/policy/rule/PhabricatorLegalpadSignaturePolicyRule.php
index 9797d5ef8..3aa7ef8d9 100644
--- a/src/applications/policy/rule/PhabricatorLegalpadSignaturePolicyRule.php
+++ b/src/applications/policy/rule/PhabricatorLegalpadSignaturePolicyRule.php
@@ -1,68 +1,77 @@
 <?php
 
 final class PhabricatorLegalpadSignaturePolicyRule
   extends PhabricatorPolicyRule {
 
   private $signatures = array();
 
   public function getRuleDescription() {
     return pht('signers of legalpad documents');
   }
 
-  public function willApplyRules(PhabricatorUser $viewer, array $values) {
+  public function willApplyRules(
+    PhabricatorUser $viewer,
+    array $values,
+    array $objects) {
+
     $values = array_unique(array_filter(array_mergev($values)));
     if (!$values) {
       return;
     }
 
     // TODO: This accepts signature of any version of the document, even an
     // older version.
 
     $documents = id(new LegalpadDocumentQuery())
       ->setViewer(PhabricatorUser::getOmnipotentUser())
       ->withPHIDs($values)
       ->withSignerPHIDs(array($viewer->getPHID()))
       ->execute();
     $this->signatures = mpull($documents, 'getPHID', 'getPHID');
   }
 
-  public function applyRule(PhabricatorUser $viewer, $value) {
+  public function applyRule(
+    PhabricatorUser $viewer,
+    $value,
+    PhabricatorPolicyInterface $object) {
+
     foreach ($value as $document_phid) {
       if (!isset($this->signatures[$document_phid])) {
         return false;
       }
     }
+
     return true;
   }
 
   public function getValueControlType() {
     return self::CONTROL_TYPE_TOKENIZER;
   }
 
   public function getValueControlTemplate() {
     return $this->getDatasourceTemplate(new LegalpadDocumentDatasource());
   }
 
   public function getRuleOrder() {
     return 900;
   }
 
   public function getValueForStorage($value) {
     PhutilTypeSpec::newFromString('list<string>')->check($value);
     return array_values($value);
   }
 
   public function getValueForDisplay(PhabricatorUser $viewer, $value) {
     $handles = id(new PhabricatorHandleQuery())
       ->setViewer($viewer)
       ->withPHIDs($value)
       ->execute();
 
     return mpull($handles, 'getFullName', 'getPHID');
   }
 
   public function ruleHasEffect($value) {
     return (bool)$value;
   }
 
 }
diff --git a/src/applications/policy/rule/PhabricatorLunarPhasePolicyRule.php b/src/applications/policy/rule/PhabricatorLunarPhasePolicyRule.php
index 24f8f2613..a3336a797 100644
--- a/src/applications/policy/rule/PhabricatorLunarPhasePolicyRule.php
+++ b/src/applications/policy/rule/PhabricatorLunarPhasePolicyRule.php
@@ -1,50 +1,54 @@
 <?php
 
 final class PhabricatorLunarPhasePolicyRule extends PhabricatorPolicyRule {
 
   const PHASE_FULL = 'full';
   const PHASE_NEW = 'new';
   const PHASE_WAXING = 'waxing';
   const PHASE_WANING = 'waning';
 
   public function getRuleDescription() {
     return pht('when the moon');
   }
 
-  public function applyRule(PhabricatorUser $viewer, $value) {
+  public function applyRule(
+    PhabricatorUser $viewer,
+    $value,
+    PhabricatorPolicyInterface $object) {
+
     $moon = new PhutilLunarPhase(PhabricatorTime::getNow());
 
     switch ($value) {
       case 'full':
         return $moon->isFull();
       case 'new':
         return $moon->isNew();
       case 'waxing':
         return $moon->isWaxing();
       case 'waning':
         return $moon->isWaning();
     }
 
     return false;
   }
 
   public function getValueControlType() {
     return self::CONTROL_TYPE_SELECT;
   }
 
   public function getValueControlTemplate() {
     return array(
       'options' => array(
         self::PHASE_FULL => pht('is full'),
         self::PHASE_NEW => pht('is new'),
         self::PHASE_WAXING => pht('is waxing'),
         self::PHASE_WANING => pht('is waning'),
       ),
     );
   }
 
   public function getRuleOrder() {
     return 1000;
   }
 
 }
diff --git a/src/applications/policy/rule/PhabricatorPolicyRule.php b/src/applications/policy/rule/PhabricatorPolicyRule.php
index eaa04e1e9..59fcc954c 100644
--- a/src/applications/policy/rule/PhabricatorPolicyRule.php
+++ b/src/applications/policy/rule/PhabricatorPolicyRule.php
@@ -1,80 +1,197 @@
 <?php
 
-abstract class PhabricatorPolicyRule {
+/**
+ * @task objectpolicy Implementing Object Policies
+ */
+abstract class PhabricatorPolicyRule extends Phobject {
 
   const CONTROL_TYPE_TEXT       = 'text';
   const CONTROL_TYPE_SELECT     = 'select';
   const CONTROL_TYPE_TOKENIZER  = 'tokenizer';
   const CONTROL_TYPE_NONE       = 'none';
 
   abstract public function getRuleDescription();
-  abstract public function applyRule(PhabricatorUser $viewer, $value);
+  abstract public function applyRule(
+    PhabricatorUser $viewer,
+    $value,
+    PhabricatorPolicyInterface $object);
 
-  public function willApplyRules(PhabricatorUser $viewer, array $values) {
+  public function willApplyRules(
+    PhabricatorUser $viewer,
+    array $values,
+    array $objects) {
     return;
   }
 
   public function getValueControlType() {
     return self::CONTROL_TYPE_TEXT;
   }
 
   public function getValueControlTemplate() {
     return null;
   }
 
+  /**
+   * Return `true` if this rule can be applied to the given object.
+   *
+   * Some policy rules may only operation on certain kinds of objects. For
+   * example, a "task author" rule
+   */
+  public function canApplyToObject(PhabricatorPolicyInterface $object) {
+    return true;
+  }
+
   protected function getDatasourceTemplate(
     PhabricatorTypeaheadDatasource $datasource) {
     return array(
       'markup' => new AphrontTokenizerTemplateView(),
       'uri' => $datasource->getDatasourceURI(),
       'placeholder' => $datasource->getPlaceholderText(),
       'browseURI' => $datasource->getBrowseURI(),
     );
   }
 
   public function getRuleOrder() {
     return 500;
   }
 
   public function getValueForStorage($value) {
     return $value;
   }
 
   public function getValueForDisplay(PhabricatorUser $viewer, $value) {
     return $value;
   }
 
   public function getRequiredHandlePHIDsForSummary($value) {
     $phids = array();
     switch ($this->getValueControlType()) {
       case self::CONTROL_TYPE_TOKENIZER:
         $phids = $value;
         break;
       case self::CONTROL_TYPE_TEXT:
       case self::CONTROL_TYPE_SELECT:
       case self::CONTROL_TYPE_NONE:
       default:
         if (phid_get_type($value) !=
             PhabricatorPHIDConstants::PHID_TYPE_UNKNOWN) {
           $phids = array($value);
         } else {
           $phids = array();
         }
         break;
     }
 
     return $phids;
   }
 
   /**
    * Return true if the given value creates a rule with a meaningful effect.
    * An example of a rule with no meaningful effect is a "users" rule with no
    * users specified.
    *
    * @return bool True if the value creates a meaningful rule.
    */
   public function ruleHasEffect($value) {
     return true;
   }
 
+
+/* -(  Transaction Hints  )-------------------------------------------------- */
+
+
+  /**
+   * Tell policy rules about upcoming transaction effects.
+   *
+   * Before transaction effects are applied, we try to stop users from making
+   * edits which will lock them out of objects. We can't do this perfectly,
+   * since they can set a policy to "the moon is full" moments before it wanes,
+   * but we try to prevent as many mistakes as possible.
+   *
+   * Some policy rules depend on complex checks against object state which
+   * we can't set up ahead of time. For example, subscriptions require database
+   * writes.
+   *
+   * In cases like this, instead of doing writes, you can pass a hint about an
+   * object to a policy rule. The rule can then look for hints and use them in
+   * rendering a verdict about whether the user will be able to see the object
+   * or not after applying the policy change.
+   *
+   * @param PhabricatorPolicyInterface Object to pass a hint about.
+   * @param PhabricatorPolicyRule Rule to pass hint to.
+   * @param wild Hint.
+   * @return void
+   */
+  public static function passTransactionHintToRule(
+    PhabricatorPolicyInterface $object,
+    PhabricatorPolicyRule $rule,
+    $hint) {
+
+    $cache = PhabricatorCaches::getRequestCache();
+    $cache->setKey(self::getObjectPolicyCacheKey($object, $rule), $hint);
+  }
+
+  protected function getTransactionHint(
+    PhabricatorPolicyInterface $object) {
+
+    $cache = PhabricatorCaches::getRequestCache();
+    return $cache->getKey(self::getObjectPolicyCacheKey($object, $this));
+  }
+
+  private static function getObjectPolicyCacheKey(
+    PhabricatorPolicyInterface $object,
+    PhabricatorPolicyRule $rule) {
+    $hash = spl_object_hash($object);
+    $rule = get_class($rule);
+    return 'policycache.'.$hash.'.'.$rule;
+  }
+
+
+/* -(  Implementing Object Policies  )--------------------------------------- */
+
+
+  /**
+   * Return a unique string like "maniphest.author" to expose this rule as an
+   * object policy.
+   *
+   * Object policy rules, like "Task Author", are more advanced than basic
+   * policy rules (like "All Users") but not as powerful as custom rules.
+   *
+   * @return string Unique identifier for this rule.
+   * @task objectpolicy
+   */
+  public function getObjectPolicyKey() {
+    return null;
+  }
+
+  public function getObjectPolicyFullKey() {
+    $key = $this->getObjectPolicyKey();
+
+    if (!$key) {
+      throw new Exception(
+        pht(
+          'This policy rule (of class "%s") does not have an associated '.
+          'object policy key.',
+          get_class($this)));
+    }
+
+    return PhabricatorPolicyQuery::OBJECT_POLICY_PREFIX.$key;
+  }
+
+  public function getObjectPolicyName() {
+    throw new PhutilMethodNotImplementedException();
+  }
+
+  public function getObjectPolicyShortName() {
+    return $this->getObjectPolicyName();
+  }
+
+  public function getObjectPolicyIcon() {
+    return 'fa-cube';
+  }
+
+  public function getPolicyExplanation() {
+    throw new PhutilMethodNotImplementedException();
+  }
+
 }
diff --git a/src/applications/policy/rule/PhabricatorProjectsPolicyRule.php b/src/applications/policy/rule/PhabricatorProjectsPolicyRule.php
index eca97ce37..c3fbfd4f7 100644
--- a/src/applications/policy/rule/PhabricatorProjectsPolicyRule.php
+++ b/src/applications/policy/rule/PhabricatorProjectsPolicyRule.php
@@ -1,66 +1,75 @@
 <?php
 
 final class PhabricatorProjectsPolicyRule extends PhabricatorPolicyRule {
 
   private $memberships = array();
 
   public function getRuleDescription() {
     return pht('members of projects');
   }
 
-  public function willApplyRules(PhabricatorUser $viewer, array $values) {
+  public function willApplyRules(
+    PhabricatorUser $viewer,
+    array $values,
+    array $objects) {
+
     $values = array_unique(array_filter(array_mergev($values)));
     if (!$values) {
       return;
     }
 
     $projects = id(new PhabricatorProjectQuery())
       ->setViewer(PhabricatorUser::getOmnipotentUser())
       ->withMemberPHIDs(array($viewer->getPHID()))
       ->withPHIDs($values)
       ->execute();
     foreach ($projects as $project) {
       $this->memberships[$viewer->getPHID()][$project->getPHID()] = true;
     }
   }
 
-  public function applyRule(PhabricatorUser $viewer, $value) {
+  public function applyRule(
+    PhabricatorUser $viewer,
+    $value,
+    PhabricatorPolicyInterface $object) {
+
     foreach ($value as $project_phid) {
       if (isset($this->memberships[$viewer->getPHID()][$project_phid])) {
         return true;
       }
     }
+
     return false;
   }
 
   public function getValueControlType() {
     return self::CONTROL_TYPE_TOKENIZER;
   }
 
   public function getValueControlTemplate() {
     return $this->getDatasourceTemplate(new PhabricatorProjectDatasource());
   }
 
   public function getRuleOrder() {
     return 200;
   }
 
   public function getValueForStorage($value) {
     PhutilTypeSpec::newFromString('list<string>')->check($value);
     return array_values($value);
   }
 
   public function getValueForDisplay(PhabricatorUser $viewer, $value) {
     $handles = id(new PhabricatorHandleQuery())
       ->setViewer($viewer)
       ->withPHIDs($value)
       ->execute();
 
     return mpull($handles, 'getFullName', 'getPHID');
   }
 
   public function ruleHasEffect($value) {
     return (bool)$value;
   }
 
 }
diff --git a/src/applications/policy/rule/PhabricatorUsersPolicyRule.php b/src/applications/policy/rule/PhabricatorUsersPolicyRule.php
index c60e43abb..aa75027cb 100644
--- a/src/applications/policy/rule/PhabricatorUsersPolicyRule.php
+++ b/src/applications/policy/rule/PhabricatorUsersPolicyRule.php
@@ -1,52 +1,57 @@
 <?php
 
 final class PhabricatorUsersPolicyRule extends PhabricatorPolicyRule {
 
   public function getRuleDescription() {
     return pht('users');
   }
 
-  public function applyRule(PhabricatorUser $viewer, $value) {
+  public function applyRule(
+    PhabricatorUser $viewer,
+    $value,
+    PhabricatorPolicyInterface $object) {
+
     foreach ($value as $phid) {
       if ($phid == $viewer->getPHID()) {
         return true;
       }
     }
+
     return false;
   }
 
   public function getValueControlType() {
     return self::CONTROL_TYPE_TOKENIZER;
   }
 
   public function getValueControlTemplate() {
     return $this->getDatasourceTemplate(new PhabricatorPeopleDatasource());
   }
 
   public function getRuleOrder() {
     return 100;
   }
 
   public function getValueForStorage($value) {
     PhutilTypeSpec::newFromString('list<string>')->check($value);
     return array_values($value);
   }
 
   public function getValueForDisplay(PhabricatorUser $viewer, $value) {
     if (!$value) {
       return array();
     }
 
     $handles = id(new PhabricatorHandleQuery())
       ->setViewer($viewer)
       ->withPHIDs($value)
       ->execute();
 
     return mpull($handles, 'getFullName', 'getPHID');
   }
 
   public function ruleHasEffect($value) {
     return (bool)$value;
   }
 
 }
diff --git a/src/applications/policy/storage/PhabricatorPolicy.php b/src/applications/policy/storage/PhabricatorPolicy.php
index c83d09ab5..e01e0dd41 100644
--- a/src/applications/policy/storage/PhabricatorPolicy.php
+++ b/src/applications/policy/storage/PhabricatorPolicy.php
@@ -1,381 +1,400 @@
 <?php
 
 final class PhabricatorPolicy
   extends PhabricatorPolicyDAO
   implements
     PhabricatorPolicyInterface,
     PhabricatorDestructibleInterface {
 
   const ACTION_ALLOW = 'allow';
   const ACTION_DENY = 'deny';
 
   private $name;
   private $shortName;
   private $type;
   private $href;
   private $workflow;
   private $icon;
 
   protected $rules = array();
   protected $defaultAction = self::ACTION_DENY;
 
   private $ruleObjects = self::ATTACHABLE;
 
   protected function getConfiguration() {
     return array(
       self::CONFIG_AUX_PHID => true,
       self::CONFIG_SERIALIZATION => array(
         'rules' => self::SERIALIZATION_JSON,
       ),
       self::CONFIG_COLUMN_SCHEMA => array(
         'defaultAction' => 'text32',
       ),
       self::CONFIG_KEY_SCHEMA => array(
         'key_phid' => null,
         'phid' => array(
           'columns' => array('phid'),
           'unique' => true,
         ),
       ),
     ) + parent::getConfiguration();
   }
 
   public function generatePHID() {
     return PhabricatorPHID::generateNewPHID(
       PhabricatorPolicyPHIDTypePolicy::TYPECONST);
   }
 
   public static function newFromPolicyAndHandle(
     $policy_identifier,
     PhabricatorObjectHandle $handle = null) {
 
     $is_global = PhabricatorPolicyQuery::isGlobalPolicy($policy_identifier);
     if ($is_global) {
       return PhabricatorPolicyQuery::getGlobalPolicy($policy_identifier);
     }
 
+    $policy = PhabricatorPolicyQuery::getObjectPolicy($policy_identifier);
+    if ($policy) {
+      return $policy;
+    }
+
     if (!$handle) {
       throw new Exception(
         pht(
           "Policy identifier is an object PHID ('%s'), but no object handle ".
           "was provided. A handle must be provided for object policies.",
           $policy_identifier));
     }
 
     $handle_phid = $handle->getPHID();
     if ($policy_identifier != $handle_phid) {
       throw new Exception(
         pht(
           "Policy identifier is an object PHID ('%s'), but the provided ".
           "handle has a different PHID ('%s'). The handle must correspond ".
           "to the policy identifier.",
           $policy_identifier,
           $handle_phid));
     }
 
     $policy = id(new PhabricatorPolicy())
       ->setPHID($policy_identifier)
       ->setHref($handle->getURI());
 
     $phid_type = phid_get_type($policy_identifier);
     switch ($phid_type) {
       case PhabricatorProjectProjectPHIDType::TYPECONST:
         $policy->setType(PhabricatorPolicyType::TYPE_PROJECT);
         $policy->setName($handle->getName());
         break;
       case PhabricatorPeopleUserPHIDType::TYPECONST:
         $policy->setType(PhabricatorPolicyType::TYPE_USER);
         $policy->setName($handle->getFullName());
         break;
       case PhabricatorPolicyPHIDTypePolicy::TYPECONST:
         // TODO: This creates a weird handle-based version of a rule policy.
         // It behaves correctly, but can't be applied since it doesn't have
         // any rules. It is used to render transactions, and might need some
         // cleanup.
         break;
       default:
         $policy->setType(PhabricatorPolicyType::TYPE_MASKED);
         $policy->setName($handle->getFullName());
         break;
     }
 
     $policy->makeEphemeral();
 
     return $policy;
   }
 
   public function setType($type) {
     $this->type = $type;
     return $this;
   }
 
   public function getType() {
     if (!$this->type) {
       return PhabricatorPolicyType::TYPE_CUSTOM;
     }
     return $this->type;
   }
 
   public function setName($name) {
     $this->name = $name;
     return $this;
   }
 
   public function getName() {
     if (!$this->name) {
       return pht('Custom Policy');
     }
     return $this->name;
   }
 
   public function setShortName($short_name) {
     $this->shortName = $short_name;
     return $this;
   }
 
   public function getShortName() {
     if ($this->shortName) {
       return $this->shortName;
     }
     return $this->getName();
   }
 
   public function setHref($href) {
     $this->href = $href;
     return $this;
   }
 
   public function getHref() {
     return $this->href;
   }
 
   public function setWorkflow($workflow) {
     $this->workflow = $workflow;
     return $this;
   }
 
   public function getWorkflow() {
     return $this->workflow;
   }
 
+  public function setIcon($icon) {
+    $this->icon = $icon;
+    return $this;
+  }
+
   public function getIcon() {
+    if ($this->icon) {
+      return $this->icon;
+    }
+
     switch ($this->getType()) {
       case PhabricatorPolicyType::TYPE_GLOBAL:
         static $map = array(
           PhabricatorPolicies::POLICY_PUBLIC  => 'fa-globe',
           PhabricatorPolicies::POLICY_USER    => 'fa-users',
           PhabricatorPolicies::POLICY_ADMIN   => 'fa-eye',
           PhabricatorPolicies::POLICY_NOONE   => 'fa-ban',
         );
         return idx($map, $this->getPHID(), 'fa-question-circle');
       case PhabricatorPolicyType::TYPE_USER:
         return 'fa-user';
       case PhabricatorPolicyType::TYPE_PROJECT:
         return 'fa-briefcase';
       case PhabricatorPolicyType::TYPE_CUSTOM:
       case PhabricatorPolicyType::TYPE_MASKED:
         return 'fa-certificate';
       default:
         return 'fa-question-circle';
     }
   }
 
   public function getSortKey() {
     return sprintf(
       '%02d%s',
       PhabricatorPolicyType::getPolicyTypeOrder($this->getType()),
       $this->getSortName());
   }
 
   private function getSortName() {
     if ($this->getType() == PhabricatorPolicyType::TYPE_GLOBAL) {
       static $map = array(
         PhabricatorPolicies::POLICY_PUBLIC  => 0,
         PhabricatorPolicies::POLICY_USER    => 1,
         PhabricatorPolicies::POLICY_ADMIN   => 2,
         PhabricatorPolicies::POLICY_NOONE   => 3,
       );
       return idx($map, $this->getPHID());
     }
     return $this->getName();
   }
 
   public static function getPolicyExplanation(
     PhabricatorUser $viewer,
     $policy) {
 
+    $rule = PhabricatorPolicyQuery::getObjectPolicyRule($policy);
+    if ($rule) {
+      return $rule->getPolicyExplanation();
+    }
+
     switch ($policy) {
       case PhabricatorPolicies::POLICY_PUBLIC:
         return pht('This object is public.');
       case PhabricatorPolicies::POLICY_USER:
         return pht('Logged in users can take this action.');
       case PhabricatorPolicies::POLICY_ADMIN:
         return pht('Administrators can take this action.');
       case PhabricatorPolicies::POLICY_NOONE:
         return pht('By default, no one can take this action.');
       default:
         $handle = id(new PhabricatorHandleQuery())
           ->setViewer($viewer)
           ->withPHIDs(array($policy))
           ->executeOne();
 
         $type = phid_get_type($policy);
         if ($type == PhabricatorProjectProjectPHIDType::TYPECONST) {
           return pht(
             'Members of the project "%s" can take this action.',
             $handle->getFullName());
         } else if ($type == PhabricatorPeopleUserPHIDType::TYPECONST) {
           return pht(
             '%s can take this action.',
             $handle->getFullName());
         } else if ($type == PhabricatorPolicyPHIDTypePolicy::TYPECONST) {
           return pht(
             'This object has a custom policy controlling who can take this '.
             'action.');
         } else {
           return pht(
             'This object has an unknown or invalid policy setting ("%s").',
             $policy);
         }
     }
   }
 
   public function getFullName() {
     switch ($this->getType()) {
       case PhabricatorPolicyType::TYPE_PROJECT:
         return pht('Project: %s', $this->getName());
       case PhabricatorPolicyType::TYPE_MASKED:
         return pht('Other: %s', $this->getName());
       default:
         return $this->getName();
     }
   }
 
   public function renderDescription($icon = false) {
     $img = null;
     if ($icon) {
       $img = id(new PHUIIconView())
         ->setIconFont($this->getIcon());
     }
 
     if ($this->getHref()) {
       $desc = javelin_tag(
         'a',
         array(
           'href' => $this->getHref(),
           'class' => 'policy-link',
           'sigil' => $this->getWorkflow() ? 'workflow' : null,
         ),
         array(
           $img,
           $this->getName(),
         ));
     } else {
       if ($img) {
         $desc = array($img, $this->getName());
       } else {
         $desc = $this->getName();
       }
     }
 
     switch ($this->getType()) {
       case PhabricatorPolicyType::TYPE_PROJECT:
         return pht('%s (Project)', $desc);
       case PhabricatorPolicyType::TYPE_CUSTOM:
         return $desc;
       case PhabricatorPolicyType::TYPE_MASKED:
         return pht(
           '%s (You do not have permission to view policy details.)',
           $desc);
       default:
         return $desc;
     }
   }
 
   /**
    * Return a list of custom rule classes (concrete subclasses of
    * @{class:PhabricatorPolicyRule}) this policy uses.
    *
    * @return list<string> List of class names.
    */
   public function getCustomRuleClasses() {
     $classes = array();
 
     foreach ($this->getRules() as $rule) {
       $class = idx($rule, 'rule');
       try {
         if (class_exists($class)) {
           $classes[$class] = $class;
         }
       } catch (Exception $ex) {
         continue;
       }
     }
 
     return array_keys($classes);
   }
 
   /**
    * Return a list of all values used by a given rule class to implement this
    * policy. This is used to bulk load data (like project memberships) in order
    * to apply policy filters efficiently.
    *
    * @param string Policy rule classname.
    * @return list<wild> List of values used in this policy.
    */
   public function getCustomRuleValues($rule_class) {
     $values = array();
     foreach ($this->getRules() as $rule) {
       if ($rule['rule'] == $rule_class) {
         $values[] = $rule['value'];
       }
     }
     return $values;
   }
 
   public function attachRuleObjects(array $objects) {
     $this->ruleObjects = $objects;
     return $this;
   }
 
   public function getRuleObjects() {
     return $this->assertAttached($this->ruleObjects);
   }
 
 
 /* -(  PhabricatorPolicyInterface  )----------------------------------------- */
 
 
   public function getCapabilities() {
     return array(
       PhabricatorPolicyCapability::CAN_VIEW,
     );
   }
 
   public function getPolicy($capability) {
     // NOTE: We implement policies only so we can comply with the interface.
     // The actual query skips them, as enforcing policies on policies seems
     // perilous and isn't currently required by the application.
     return PhabricatorPolicies::POLICY_PUBLIC;
   }
 
   public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
     return false;
   }
 
   public function describeAutomaticCapability($capability) {
     return null;
   }
 
 
 /* -(  PhabricatorDestructibleInterface  )----------------------------------- */
 
 
   public function destroyObjectPermanently(
     PhabricatorDestructionEngine $engine) {
 
     $this->delete();
   }
 
 
 }
diff --git a/src/applications/ponder/constants/PonderConstants.php b/src/applications/ponder/constants/PonderConstants.php
index ca281620c..e096cc0a5 100644
--- a/src/applications/ponder/constants/PonderConstants.php
+++ b/src/applications/ponder/constants/PonderConstants.php
@@ -1,3 +1,3 @@
 <?php
 
-abstract class PonderConstants {}
+abstract class PonderConstants extends Phobject {}
diff --git a/src/applications/ponder/editor/PonderVoteEditor.php b/src/applications/ponder/editor/PonderVoteEditor.php
index 2edb18678..58212a08d 100644
--- a/src/applications/ponder/editor/PonderVoteEditor.php
+++ b/src/applications/ponder/editor/PonderVoteEditor.php
@@ -1,77 +1,77 @@
 <?php
 
 final class PonderVoteEditor extends PhabricatorEditor {
 
   private $answer;
   private $votable;
   private $anwer;
   private $vote;
 
   public function setAnswer($answer) {
     $this->answer = $answer;
     return $this;
   }
 
   public function setVotable($votable) {
     $this->votable = $votable;
     return $this;
   }
 
   public function setVote($vote) {
     $this->vote = $vote;
     return $this;
   }
 
   public function saveVote() {
     $actor = $this->requireActor();
     if (!$this->votable) {
-      throw new Exception(pht('Must set votable before saving vote.'));
+      throw new PhutilInvalidStateException('setVotable');
     }
 
     $votable = $this->votable;
     $newvote = $this->vote;
 
     // prepare vote add, or update if this user is amending an
     // earlier vote
     $editor = id(new PhabricatorEdgeEditor())
       ->addEdge(
         $actor->getPHID(),
         $votable->getUserVoteEdgeType(),
         $votable->getVotablePHID(),
         array('data' => $newvote))
       ->removeEdge(
         $actor->getPHID(),
         $votable->getUserVoteEdgeType(),
         $votable->getVotablePHID());
 
     $conn = $votable->establishConnection('w');
     $trans = $conn->openTransaction();
     $trans->beginReadLocking();
 
       $votable->reload();
       $curvote = (int)PhabricatorEdgeQuery::loadSingleEdgeData(
         $actor->getPHID(),
         $votable->getUserVoteEdgeType(),
         $votable->getVotablePHID());
 
       if (!$curvote) {
         $curvote = PonderVote::VOTE_NONE;
       }
 
       // Adjust votable's score by this much.
       $delta = $newvote - $curvote;
 
       queryfx($conn,
         'UPDATE %T as t
-        SET t.`voteCount` = t.`voteCount` + %d
-        WHERE t.`PHID` = %s',
+        SET t.voteCount = t.voteCount + %d
+        WHERE t.PHID = %s',
         $votable->getTableName(),
         $delta,
         $votable->getVotablePHID());
 
       $editor->save();
 
     $trans->endReadLocking();
     $trans->saveTransaction();
   }
 }
diff --git a/src/applications/project/application/PhabricatorProjectApplication.php b/src/applications/project/application/PhabricatorProjectApplication.php
index e7e4f623f..adf6378e2 100644
--- a/src/applications/project/application/PhabricatorProjectApplication.php
+++ b/src/applications/project/application/PhabricatorProjectApplication.php
@@ -1,142 +1,142 @@
 <?php
 
 final class PhabricatorProjectApplication extends PhabricatorApplication {
 
   public function getName() {
     return pht('Projects');
   }
 
   public function getShortDescription() {
     return pht('Get Organized');
   }
 
   public function isPinnedByDefault(PhabricatorUser $viewer) {
     return true;
   }
 
   public function getBaseURI() {
     return '/project/';
   }
 
   public function getFontIcon() {
     return 'fa-briefcase';
   }
 
   public function getFlavorText() {
     return pht('Group stuff into big piles.');
   }
 
   public function getRemarkupRules() {
     return array(
       new ProjectRemarkupRule(),
     );
   }
 
   public function getEventListeners() {
     return array(
       new PhabricatorProjectUIEventListener(),
     );
   }
 
   public function getRoutes() {
     return array(
       '/project/' => array(
         '(?:query/(?P<queryKey>[^/]+)/)?' => 'PhabricatorProjectListController',
         'filter/(?P<filter>[^/]+)/' => 'PhabricatorProjectListController',
         'details/(?P<id>[1-9]\d*)/'
           => 'PhabricatorProjectEditDetailsController',
         'archive/(?P<id>[1-9]\d*)/'
           => 'PhabricatorProjectArchiveController',
         'members/(?P<id>[1-9]\d*)/'
           => 'PhabricatorProjectMembersEditController',
         'members/(?P<id>[1-9]\d*)/remove/'
           => 'PhabricatorProjectMembersRemoveController',
         'profile/(?P<id>[1-9]\d*)/'
           => 'PhabricatorProjectProfileController',
         'feed/(?P<id>[1-9]\d*)/'
           => 'PhabricatorProjectFeedController',
         'view/(?P<id>[1-9]\d*)/'
           => 'PhabricatorProjectViewController',
         'picture/(?P<id>[1-9]\d*)/'
           => 'PhabricatorProjectEditPictureController',
         'icon/(?P<id>[1-9]\d*)/'
           => 'PhabricatorProjectEditIconController',
         'icon/'
           => 'PhabricatorProjectEditIconController',
         'create/' => 'PhabricatorProjectEditDetailsController',
         'board/(?P<id>[1-9]\d*)/'.
           '(?P<filter>filter/)?'.
           '(?:query/(?P<queryKey>[^/]+)/)?'
           => 'PhabricatorProjectBoardViewController',
         'move/(?P<id>[1-9]\d*)/' => 'PhabricatorProjectMoveController',
         'board/(?P<projectID>[1-9]\d*)/' => array(
           'edit/(?:(?P<id>\d+)/)?'
             => 'PhabricatorProjectColumnEditController',
           'hide/(?:(?P<id>\d+)/)?'
             => 'PhabricatorProjectColumnHideController',
           'column/(?:(?P<id>\d+)/)?'
             => 'PhabricatorProjectColumnDetailController',
           'import/'
             => 'PhabricatorProjectBoardImportController',
           'reorder/'
             => 'PhabricatorProjectBoardReorderController',
         ),
         'update/(?P<id>[1-9]\d*)/(?P<action>[^/]+)/'
           => 'PhabricatorProjectUpdateController',
         'history/(?P<id>[1-9]\d*)/' => 'PhabricatorProjectHistoryController',
         '(?P<action>watch|unwatch)/(?P<id>[1-9]\d*)/'
           => 'PhabricatorProjectWatchController',
       ),
       '/tag/' => array(
         '(?P<slug>[^/]+)/' => 'PhabricatorProjectViewController',
         '(?P<slug>[^/]+)/board/' => 'PhabricatorProjectBoardViewController',
       ),
     );
   }
 
   public function getQuickCreateItems(PhabricatorUser $viewer) {
     $can_create = PhabricatorPolicyFilter::hasCapability(
       $viewer,
       $this,
       ProjectCreateProjectsCapability::CAPABILITY);
 
     $items = array();
     if ($can_create) {
       $item = id(new PHUIListItemView())
         ->setName(pht('Project'))
         ->setIcon('fa-briefcase')
         ->setHref($this->getBaseURI().'create/');
       $items[] = $item;
     }
 
     return $items;
   }
 
   protected function getCustomCapabilities() {
     return array(
       ProjectCreateProjectsCapability::CAPABILITY => array(),
       ProjectCanLockProjectsCapability::CAPABILITY => array(
         'default' => PhabricatorPolicies::POLICY_ADMIN,
       ),
       ProjectDefaultViewCapability::CAPABILITY => array(
-        'caption' => pht(
-          'Default view policy for newly created projects.'),
+        'caption' => pht('Default view policy for newly created projects.'),
+        'template' => PhabricatorProjectProjectPHIDType::TYPECONST,
       ),
       ProjectDefaultEditCapability::CAPABILITY => array(
-        'caption' => pht(
-          'Default edit policy for newly created projects.'),
+        'caption' => pht('Default edit policy for newly created projects.'),
+        'template' => PhabricatorProjectProjectPHIDType::TYPECONST,
       ),
       ProjectDefaultJoinCapability::CAPABILITY => array(
-        'caption' => pht(
-          'Default join policy for newly created projects.'),
+        'caption' => pht('Default join policy for newly created projects.'),
+        'template' => PhabricatorProjectProjectPHIDType::TYPECONST,
       ),
     );
   }
 
   public function getApplicationSearchDocumentTypes() {
     return array(
       PhabricatorProjectProjectPHIDType::TYPECONST,
     );
   }
 
 }
diff --git a/src/applications/project/constants/PhabricatorProjectStatus.php b/src/applications/project/constants/PhabricatorProjectStatus.php
index 16f4a9833..79b1ee823 100644
--- a/src/applications/project/constants/PhabricatorProjectStatus.php
+++ b/src/applications/project/constants/PhabricatorProjectStatus.php
@@ -1,24 +1,24 @@
 <?php
 
-final class PhabricatorProjectStatus {
+final class PhabricatorProjectStatus extends Phobject {
 
   const STATUS_ACTIVE       = 0;
   const STATUS_ARCHIVED     = 100;
 
   public static function getNameForStatus($status) {
     $map = array(
       self::STATUS_ACTIVE     => pht('Active'),
       self::STATUS_ARCHIVED   => pht('Archived'),
     );
 
     return idx($map, coalesce($status, '?'), pht('Unknown'));
   }
 
   public static function getStatusMap() {
     return array(
       self::STATUS_ACTIVE   => pht('Active'),
       self::STATUS_ARCHIVED => pht('Archived'),
     );
   }
 
 }
diff --git a/src/applications/project/controller/PhabricatorProjectProfileController.php b/src/applications/project/controller/PhabricatorProjectProfileController.php
index cb8fe9e87..2420297c2 100644
--- a/src/applications/project/controller/PhabricatorProjectProfileController.php
+++ b/src/applications/project/controller/PhabricatorProjectProfileController.php
@@ -1,222 +1,223 @@
 <?php
 
 final class PhabricatorProjectProfileController
   extends PhabricatorProjectController {
 
   public function shouldAllowPublic() {
     return true;
   }
 
   public function handleRequest(AphrontRequest $request) {
     $user = $request->getUser();
 
     $query = id(new PhabricatorProjectQuery())
       ->setViewer($user)
       ->needMembers(true)
       ->needWatchers(true)
       ->needImages(true)
       ->needSlugs(true);
     $id = $request->getURIData('id');
     $slug = $request->getURIData('slug');
     if ($slug) {
       $query->withSlugs(array($slug));
     } else {
       $query->withIDs(array($id));
     }
     $project = $query->executeOne();
     if (!$project) {
       return new Aphront404Response();
     }
     if ($slug && $slug != $project->getPrimarySlug()) {
       return id(new AphrontRedirectResponse())
         ->setURI('/tag/'.$project->getPrimarySlug().'/');
     }
 
     $picture = $project->getProfileImageURI();
 
     $header = id(new PHUIHeaderView())
       ->setHeader($project->getName())
       ->setUser($user)
       ->setPolicyObject($project)
       ->setImage($picture);
 
     if ($project->getStatus() == PhabricatorProjectStatus::STATUS_ACTIVE) {
       $header->setStatus('fa-check', 'bluegrey', pht('Active'));
     } else {
       $header->setStatus('fa-ban', 'red', pht('Archived'));
     }
 
     $actions = $this->buildActionListView($project);
     $properties = $this->buildPropertyListView($project, $actions);
 
     $object_box = id(new PHUIObjectBoxView())
       ->setHeader($header)
       ->addPropertyList($properties);
 
     $timeline = $this->buildTransactionTimeline(
       $project,
       new PhabricatorProjectTransactionQuery());
     $timeline->setShouldTerminate(true);
 
     $nav = $this->buildIconNavView($project);
     $nav->selectFilter("profile/{$id}/");
     $nav->appendChild($object_box);
     $nav->appendChild($timeline);
 
     return $this->buildApplicationPage(
       $nav,
       array(
         'title' => $project->getName(),
         'pageObjects' => array($project->getPHID()),
       ));
   }
 
   private function buildActionListView(PhabricatorProject $project) {
     $request = $this->getRequest();
     $viewer = $request->getUser();
 
     $id = $project->getID();
 
     $view = id(new PhabricatorActionListView())
       ->setUser($viewer)
       ->setObject($project)
       ->setObjectURI($request->getRequestURI());
 
     $can_edit = PhabricatorPolicyFilter::hasCapability(
       $viewer,
       $project,
       PhabricatorPolicyCapability::CAN_EDIT);
 
     $view->addAction(
       id(new PhabricatorActionView())
         ->setName(pht('Edit Details'))
         ->setIcon('fa-pencil')
-        ->setHref($this->getApplicationURI("details/{$id}/")));
+        ->setHref($this->getApplicationURI("details/{$id}/"))
+        ->setDisabled(!$can_edit));
 
     $view->addAction(
       id(new PhabricatorActionView())
         ->setName(pht('Edit Picture'))
         ->setIcon('fa-picture-o')
         ->setHref($this->getApplicationURI("picture/{$id}/"))
         ->setDisabled(!$can_edit)
         ->setWorkflow(!$can_edit));
 
     if ($project->isArchived()) {
       $view->addAction(
         id(new PhabricatorActionView())
           ->setName(pht('Activate Project'))
           ->setIcon('fa-check')
           ->setHref($this->getApplicationURI("archive/{$id}/"))
           ->setDisabled(!$can_edit)
           ->setWorkflow(true));
     } else {
       $view->addAction(
         id(new PhabricatorActionView())
           ->setName(pht('Archive Project'))
           ->setIcon('fa-ban')
           ->setHref($this->getApplicationURI("archive/{$id}/"))
           ->setDisabled(!$can_edit)
           ->setWorkflow(true));
     }
 
     $action = null;
     if (!$project->isUserMember($viewer->getPHID())) {
       $can_join = PhabricatorPolicyFilter::hasCapability(
         $viewer,
         $project,
         PhabricatorPolicyCapability::CAN_JOIN);
 
       $action = id(new PhabricatorActionView())
         ->setUser($viewer)
         ->setRenderAsForm(true)
         ->setHref('/project/update/'.$project->getID().'/join/')
         ->setIcon('fa-plus')
         ->setDisabled(!$can_join)
         ->setName(pht('Join Project'));
       $view->addAction($action);
     } else {
       $action = id(new PhabricatorActionView())
         ->setWorkflow(true)
         ->setHref('/project/update/'.$project->getID().'/leave/')
         ->setIcon('fa-times')
         ->setName(pht('Leave Project...'));
       $view->addAction($action);
 
       if (!$project->isUserWatcher($viewer->getPHID())) {
         $action = id(new PhabricatorActionView())
           ->setWorkflow(true)
           ->setHref('/project/watch/'.$project->getID().'/')
           ->setIcon('fa-eye')
           ->setName(pht('Watch Project'));
         $view->addAction($action);
       } else {
         $action = id(new PhabricatorActionView())
           ->setWorkflow(true)
           ->setHref('/project/unwatch/'.$project->getID().'/')
           ->setIcon('fa-eye-slash')
           ->setName(pht('Unwatch Project'));
         $view->addAction($action);
       }
     }
 
     return $view;
   }
 
   private function buildPropertyListView(
     PhabricatorProject $project,
     PhabricatorActionListView $actions) {
     $request = $this->getRequest();
     $viewer = $request->getUser();
 
     $view = id(new PHUIPropertyListView())
       ->setUser($viewer)
       ->setObject($project)
       ->setActionList($actions);
 
     $hashtags = array();
     foreach ($project->getSlugs() as $slug) {
       $hashtags[] = id(new PHUITagView())
         ->setType(PHUITagView::TYPE_OBJECT)
         ->setName('#'.$slug->getSlug());
     }
 
     $view->addProperty(pht('Hashtags'), phutil_implode_html(' ', $hashtags));
 
     $view->addProperty(
       pht('Members'),
       $project->getMemberPHIDs()
         ? $viewer
           ->renderHandleList($project->getMemberPHIDs())
           ->setAsInline(true)
         : phutil_tag('em', array(), pht('None')));
 
     $view->addProperty(
       pht('Watchers'),
       $project->getWatcherPHIDs()
         ? $viewer
           ->renderHandleList($project->getWatcherPHIDs())
           ->setAsInline(true)
         : phutil_tag('em', array(), pht('None')));
 
     $descriptions = PhabricatorPolicyQuery::renderPolicyDescriptions(
       $viewer,
       $project);
 
     $view->addProperty(
       pht('Looks Like'),
       $viewer->renderHandle($project->getPHID())->setAsTag(true));
 
     $view->addProperty(
       pht('Joinable By'),
       $descriptions[PhabricatorPolicyCapability::CAN_JOIN]);
 
     $field_list = PhabricatorCustomField::getObjectFields(
       $project,
       PhabricatorCustomField::ROLE_VIEW);
     $field_list->appendFieldsToPropertyList($project, $viewer, $view);
 
     return $view;
   }
 
 
 }
diff --git a/src/applications/project/view/ProjectBoardTaskCard.php b/src/applications/project/view/ProjectBoardTaskCard.php
index a601608eb..4fc26988d 100644
--- a/src/applications/project/view/ProjectBoardTaskCard.php
+++ b/src/applications/project/view/ProjectBoardTaskCard.php
@@ -1,77 +1,79 @@
 <?php
 
-final class ProjectBoardTaskCard {
+final class ProjectBoardTaskCard extends Phobject {
 
   private $viewer;
   private $task;
   private $owner;
   private $canEdit;
 
   public function setViewer(PhabricatorUser $viewer) {
     $this->viewer = $viewer;
     return $this;
   }
   public function getViewer() {
     return $this->viewer;
   }
 
   public function setTask(ManiphestTask $task) {
     $this->task = $task;
     return $this;
   }
   public function getTask() {
     return $this->task;
   }
 
   public function setOwner(PhabricatorObjectHandle $owner = null) {
     $this->owner = $owner;
     return $this;
   }
   public function getOwner() {
     return $this->owner;
   }
 
   public function setCanEdit($can_edit) {
     $this->canEdit = $can_edit;
     return $this;
   }
 
   public function getCanEdit() {
     return $this->canEdit;
   }
 
   public function getItem() {
     $task = $this->getTask();
     $owner = $this->getOwner();
     $can_edit = $this->getCanEdit();
 
     $color_map = ManiphestTaskPriority::getColorMap();
     $bar_color = idx($color_map, $task->getPriority(), 'grey');
 
     $card = id(new PHUIObjectItemView())
+      ->setObject($task)
+      ->setUser($this->getViewer())
       ->setObjectName('T'.$task->getID())
       ->setHeader($task->getTitle())
       ->setGrippable($can_edit)
       ->setHref('/T'.$task->getID())
       ->addSigil('project-card')
       ->setDisabled($task->isClosed())
       ->setMetadata(
         array(
           'objectPHID' => $task->getPHID(),
         ))
       ->addAction(
         id(new PHUIListItemView())
         ->setName(pht('Edit'))
         ->setIcon('fa-pencil')
         ->addSigil('edit-project-card')
         ->setHref('/maniphest/task/edit/'.$task->getID().'/'))
       ->setBarColor($bar_color);
 
     if ($owner) {
       $card->addAttribute($owner->renderLink());
     }
 
     return $card;
   }
 
 }
diff --git a/src/applications/releeph/commitfinder/ReleephCommitFinder.php b/src/applications/releeph/commitfinder/ReleephCommitFinder.php
index 89fd84e19..8c1712c80 100644
--- a/src/applications/releeph/commitfinder/ReleephCommitFinder.php
+++ b/src/applications/releeph/commitfinder/ReleephCommitFinder.php
@@ -1,120 +1,120 @@
 <?php
 
-final class ReleephCommitFinder {
+final class ReleephCommitFinder extends Phobject {
 
   private $releephProject;
   private $user;
   private $objectPHID;
 
   public function setUser(PhabricatorUser $user) {
     $this->user = $user;
     return $this;
   }
   public function getUser() {
     return $this->user;
   }
 
   public function setReleephProject(ReleephProject $rp) {
     $this->releephProject = $rp;
     return $this;
   }
 
   public function getRequestedObjectPHID() {
     return $this->objectPHID;
   }
 
   public function fromPartial($partial_string) {
     $this->objectPHID = null;
 
     // Look for diffs
     $matches = array();
     if (preg_match('/^D([1-9]\d*)$/', $partial_string, $matches)) {
       $diff_id = $matches[1];
       $diff_rev = id(new DifferentialRevisionQuery())
         ->setViewer($this->getUser())
         ->withIDs(array($diff_id))
         ->needCommitPHIDs(true)
         ->executeOne();
       if (!$diff_rev) {
         throw new ReleephCommitFinderException(
           pht(
             '%s does not refer to an existing diff.',
             $partial_string));
       }
       $commit_phids = $diff_rev->getCommitPHIDs();
 
       if (!$commit_phids) {
         throw new ReleephCommitFinderException(
           pht(
             '%s has no commits associated with it yet.',
             $partial_string));
       }
 
       $this->objectPHID = $diff_rev->getPHID();
 
       $commits = id(new DiffusionCommitQuery())
         ->setViewer($this->getUser())
         ->withPHIDs($commit_phids)
         ->execute();
       $commits = msort($commits, 'getEpoch');
       return head($commits);
     }
 
     // Look for a raw commit number, or r<callsign><commit-number>.
     $repository = $this->releephProject->getRepository();
     $dr_data = null;
     $matches = array();
     if (preg_match('/^r(?P<callsign>[A-Z]+)(?P<commit>\w+)$/',
       $partial_string, $matches)) {
       $callsign = $matches['callsign'];
       if ($callsign != $repository->getCallsign()) {
         throw new ReleephCommitFinderException(
           pht(
             '%s is in a different repository to this Releeph project (%s).',
             $partial_string,
             $repository->getCallsign()));
       } else {
         $dr_data = $matches;
       }
     } else {
       $dr_data = array(
         'callsign' => $repository->getCallsign(),
         'commit' => $partial_string,
       );
     }
 
     try {
       $dr_data['user'] = $this->getUser();
       $dr = DiffusionRequest::newFromDictionary($dr_data);
     } catch (Exception $ex) {
       $message = pht(
         'No commit matches %s: %s',
         $partial_string,
         $ex->getMessage());
       throw new ReleephCommitFinderException($message);
     }
 
     $phabricator_repository_commit = $dr->loadCommit();
 
     if (!$phabricator_repository_commit) {
       throw new ReleephCommitFinderException(
         pht(
           "The commit %s doesn't exist in this repository.",
           $partial_string));
     }
 
     // When requesting a single commit, if it has an associated review we
     // imply the review was requested instead. This is always correct for now
     // and consistent with the older behavior, although it might not be the
     // right rule in the future.
     $phids = PhabricatorEdgeQuery::loadDestinationPHIDs(
       $phabricator_repository_commit->getPHID(),
       DiffusionCommitHasRevisionEdgeType::EDGECONST);
     if ($phids) {
       $this->objectPHID = head($phids);
     }
 
     return $phabricator_repository_commit;
   }
 
 }
diff --git a/src/applications/releeph/constants/ReleephRequestStatus.php b/src/applications/releeph/constants/ReleephRequestStatus.php
index 327e7923a..2d29a9592 100644
--- a/src/applications/releeph/constants/ReleephRequestStatus.php
+++ b/src/applications/releeph/constants/ReleephRequestStatus.php
@@ -1,32 +1,32 @@
 <?php
 
-final class ReleephRequestStatus {
+final class ReleephRequestStatus extends Phobject {
 
   const STATUS_REQUESTED       = 1;
   const STATUS_NEEDS_PICK      = 2;  // aka approved
   const STATUS_REJECTED        = 3;
   const STATUS_ABANDONED       = 4;
   const STATUS_PICKED          = 5;
   const STATUS_REVERTED        = 6;
   const STATUS_NEEDS_REVERT    = 7;  // aka revert requested
 
   public static function getStatusDescriptionFor($status) {
     $descriptions = array(
       self::STATUS_REQUESTED       => pht('Requested'),
       self::STATUS_REJECTED        => pht('Rejected'),
       self::STATUS_ABANDONED       => pht('Abandoned'),
       self::STATUS_PICKED          => pht('Pulled'),
       self::STATUS_REVERTED        => pht('Reverted'),
       self::STATUS_NEEDS_PICK      => pht('Needs Pull'),
       self::STATUS_NEEDS_REVERT    => pht('Needs Revert'),
     );
     return idx($descriptions, $status, '??');
   }
 
   public static function getStatusClassSuffixFor($status) {
     $description = self::getStatusDescriptionFor($status);
     $class = str_replace(' ', '-', strtolower($description));
     return $class;
   }
 
 }
diff --git a/src/applications/releeph/differential/DifferentialReleephRequestFieldSpecification.php b/src/applications/releeph/differential/DifferentialReleephRequestFieldSpecification.php
index 3db133a3e..2f06f50f1 100644
--- a/src/applications/releeph/differential/DifferentialReleephRequestFieldSpecification.php
+++ b/src/applications/releeph/differential/DifferentialReleephRequestFieldSpecification.php
@@ -1,386 +1,386 @@
 <?php
 
 /**
  * This DifferentialFieldSpecification exists for two reason:
  *
  * 1: To parse "Releeph: picks RQ<nn>" headers in commits created by
  * arc-releeph so that RQs committed by arc-releeph have real
  * PhabricatorRepositoryCommits associated with them (instaed of just the SHA
  * of the commit, as seen by the pusher).
  *
  * 2: If requestors want to commit directly to their release branch, they can
  * use this header to (i) indicate on a differential revision that this
  * differential revision is for the release branch, and (ii) when they land
  * their diff on to the release branch manually, the ReleephRequest is
  * automatically updated (instead of having to use the "Mark Manually Picked"
  * button.)
  *
  */
-final class DifferentialReleephRequestFieldSpecification {
+final class DifferentialReleephRequestFieldSpecification extends Phobject {
 
   // TODO: This class is essentially dead right now, see T2222.
 
   const ACTION_PICKS    = 'picks';
   const ACTION_REVERTS  = 'reverts';
 
   private $releephAction;
   private $releephPHIDs = array();
 
   public function getStorageKey() {
     return 'releeph:actions';
   }
 
   public function getValueForStorage() {
     return json_encode(array(
       'releephAction' => $this->releephAction,
       'releephPHIDs'  => $this->releephPHIDs,
     ));
   }
 
   public function setValueFromStorage($json) {
     if ($json) {
       $dict = phutil_json_decode($json);
       $this->releephAction = idx($dict, 'releephAction');
       $this->releephPHIDs = idx($dict, 'releephPHIDs');
     }
     return $this;
   }
 
   public function shouldAppearOnRevisionView() {
     return true;
   }
 
   public function renderLabelForRevisionView() {
     return pht('Releeph');
   }
 
   public function getRequiredHandlePHIDs() {
     return mpull($this->loadReleephRequests(), 'getPHID');
   }
 
   public function renderValueForRevisionView() {
     static $tense;
 
     if ($tense === null) {
       $tense = array(
         self::ACTION_PICKS => array(
           'future'  => pht('Will pick'),
           'past'    => pht('Picked'),
         ),
         self::ACTION_REVERTS => array(
           'future'  => pht('Will revert'),
           'past'    => pht('Reverted'),
         ),
       );
     }
 
     $releeph_requests = $this->loadReleephRequests();
     if (!$releeph_requests) {
       return null;
     }
 
     $status = $this->getRevision()->getStatus();
     if ($status == ArcanistDifferentialRevisionStatus::CLOSED) {
       $verb = $tense[$this->releephAction]['past'];
     } else {
       $verb = $tense[$this->releephAction]['future'];
     }
 
     $parts = hsprintf('%s...', $verb);
     foreach ($releeph_requests as $releeph_request) {
       $parts->appendHTML(phutil_tag('br'));
       $parts->appendHTML(
         $this->getHandle($releeph_request->getPHID())->renderLink());
     }
 
     return $parts;
   }
 
   public function shouldAppearOnCommitMessage() {
     return true;
   }
 
   public function getCommitMessageKey() {
     return 'releephActions';
   }
 
   public function setValueFromParsedCommitMessage($dict) {
     $this->releephAction = $dict['releephAction'];
     $this->releephPHIDs = $dict['releephPHIDs'];
     return $this;
   }
 
   public function renderValueForCommitMessage($is_edit) {
     $releeph_requests = $this->loadReleephRequests();
     if (!$releeph_requests) {
       return null;
     }
 
     $parts = array($this->releephAction);
     foreach ($releeph_requests as $releeph_request) {
       $parts[] = 'RQ'.$releeph_request->getID();
     }
 
     return implode(' ', $parts);
   }
 
   /**
    * Releeph fields should look like:
    *
    *   Releeph: picks RQ1 RQ2, RQ3
    *   Releeph: reverts RQ1
    */
   public function parseValueFromCommitMessage($value) {
     /**
      * Releeph commit messages look like this (but with more blank lines,
      * omitted here):
      *
      *   Make CaptainHaddock more reasonable
      *   Releeph: picks RQ1
      *   Requested By: edward
      *   Approved By: edward (requestor)
      *   Request Reason: x
      *   Summary: Make the Haddock implementation more reasonable.
      *   Test Plan: none
      *   Reviewers: user1
      *
      * Some of these fields are recognized by Differential (e.g. "Requested
      * By"). They are folded up into the "Releeph" field, parsed by this
      * class. As such $value includes more than just the first-line:
      *
      *   "picks RQ1\n\nRequested By: edward\n\nApproved By: edward (requestor)"
      *
      * To hack around this, just consider the first line of $value when
      * determining what Releeph actions the parsed commit is performing.
      */
     $first_line = head(array_filter(explode("\n", $value)));
 
     $tokens = preg_split('/\s*,?\s+/', $first_line);
     $raw_action = array_shift($tokens);
     $action = strtolower($raw_action);
 
     if (!$action) {
       return null;
     }
 
     switch ($action) {
       case self::ACTION_REVERTS:
       case self::ACTION_PICKS:
         break;
 
       default:
         throw new DifferentialFieldParseException(
           pht(
             "Commit message contains unknown Releeph action '%s'!",
             $raw_action));
         break;
     }
 
     $releeph_requests = array();
     foreach ($tokens as $token) {
       $match = array();
       if (!preg_match('/^(?:RQ)?(\d+)$/i', $token, $match)) {
         $label = $this->renderLabelForCommitMessage();
         throw new DifferentialFieldParseException(
           pht(
             "Commit message contains unparseable ".
             "Releeph request token '%s'!",
             $token));
       }
 
       $id = (int)$match[1];
       $releeph_request = id(new ReleephRequest())->load($id);
 
       if (!$releeph_request) {
         throw new DifferentialFieldParseException(
           pht(
             'Commit message references non existent Releeph request: %s!',
             $value));
       }
 
       $releeph_requests[] = $releeph_request;
     }
 
     if (count($releeph_requests) > 1) {
       $rqs_seen = array();
       $groups = array();
       foreach ($releeph_requests as $releeph_request) {
         $releeph_branch = $releeph_request->getBranch();
         $branch_name = $releeph_branch->getName();
         $rq_id = 'RQ'.$releeph_request->getID();
 
         if (idx($rqs_seen, $rq_id)) {
           throw new DifferentialFieldParseException(
             pht(
               'Commit message refers to %s multiple times!',
               $rq_id));
         }
         $rqs_seen[$rq_id] = true;
 
         if (!isset($groups[$branch_name])) {
           $groups[$branch_name] = array();
         }
         $groups[$branch_name][] = $rq_id;
       }
 
       if (count($groups) > 1) {
         $lists = array();
         foreach ($groups as $branch_name => $rq_ids) {
           $lists[] = implode(', ', $rq_ids).' in '.$branch_name;
         }
         throw new DifferentialFieldParseException(
           pht(
             'Commit message references multiple Releeph requests, '.
             'but the requests are in different branches: %s',
             implode('; ', $lists)));
       }
     }
 
     $phids = mpull($releeph_requests, 'getPHID');
 
     $data = array(
       'releephAction' => $action,
       'releephPHIDs'  => $phids,
     );
     return $data;
   }
 
   public function renderLabelForCommitMessage() {
     return pht('Releeph');
   }
 
   public function shouldAppearOnCommitMessageTemplate() {
     return false;
   }
 
   public function didParseCommit(
     PhabricatorRepository $repo,
     PhabricatorRepositoryCommit $commit,
     PhabricatorRepositoryCommitData $data) {
 
     // NOTE: This is currently dead code. See T2222.
 
     $releeph_requests = $this->loadReleephRequests();
 
     if (!$releeph_requests) {
       return;
     }
 
     $releeph_branch = head($releeph_requests)->getBranch();
     if (!$this->isCommitOnBranch($repo, $commit, $releeph_branch)) {
       return;
     }
 
     foreach ($releeph_requests as $releeph_request) {
       if ($this->releephAction === self::ACTION_PICKS) {
         $action = 'pick';
       } else {
         $action = 'revert';
       }
 
       $actor_phid = coalesce(
         $data->getCommitDetail('committerPHID'),
         $data->getCommitDetail('authorPHID'));
 
       $actor = id(new PhabricatorUser())
         ->loadOneWhere('phid = %s', $actor_phid);
 
       $xactions = array();
 
       $xactions[] = id(new ReleephRequestTransaction())
         ->setTransactionType(ReleephRequestTransaction::TYPE_DISCOVERY)
         ->setMetadataValue('action', $action)
         ->setMetadataValue('authorPHID',
           $data->getCommitDetail('authorPHID'))
         ->setMetadataValue('committerPHID',
           $data->getCommitDetail('committerPHID'))
         ->setNewValue($commit->getPHID());
 
       $editor = id(new ReleephRequestTransactionalEditor())
         ->setActor($actor)
         ->setContinueOnNoEffect(true)
         ->setContentSource(
           PhabricatorContentSource::newForSource(
             PhabricatorContentSource::SOURCE_UNKNOWN,
             array()));
 
       $editor->applyTransactions($releeph_request, $xactions);
     }
   }
 
   private function loadReleephRequests() {
     if (!$this->releephPHIDs) {
       return array();
     }
 
     return id(new ReleephRequestQuery())
       ->setViewer($this->getViewer())
       ->withPHIDs($this->releephPHIDs)
       ->execute();
   }
 
   private function isCommitOnBranch(
     PhabricatorRepository $repo,
     PhabricatorRepositoryCommit $commit,
     ReleephBranch $releeph_branch) {
 
     switch ($repo->getVersionControlSystem()) {
       case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
         list($output) = $repo->execxLocalCommand(
           'branch --all --no-color --contains %s',
           $commit->getCommitIdentifier());
 
         $remote_prefix = 'remotes/origin/';
         $branches = array();
         foreach (array_filter(explode("\n", $output)) as $line) {
           $tokens = explode(' ', $line);
           $ref = last($tokens);
           if (strncmp($ref, $remote_prefix, strlen($remote_prefix)) === 0) {
             $branch = substr($ref, strlen($remote_prefix));
             $branches[$branch] = $branch;
           }
         }
 
         return idx($branches, $releeph_branch->getName());
         break;
 
       case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
         $change_query = DiffusionPathChangeQuery::newFromDiffusionRequest(
           DiffusionRequest::newFromDictionary(array(
             'user' => $this->getUser(),
             'repository' => $repo,
             'commit' => $commit->getCommitIdentifier(),
           )));
         $path_changes = $change_query->loadChanges();
         $commit_paths = mpull($path_changes, 'getPath');
 
         $branch_path = $releeph_branch->getName();
 
         $in_branch = array();
         $ex_branch = array();
         foreach ($commit_paths as $path) {
           if (strncmp($path, $branch_path, strlen($branch_path)) === 0) {
             $in_branch[] = $path;
           } else {
             $ex_branch[] = $path;
           }
         }
 
         if ($in_branch && $ex_branch) {
           $error = pht(
             'CONFUSION: commit %s in %s contains %d path change(s) that were '.
             'part of a Releeph branch, but also has %d path change(s) not '.
             'part of a Releeph branch!',
             $commit->getCommitIdentifier(),
             $repo->getCallsign(),
             count($in_branch),
             count($ex_branch));
           phlog($error);
         }
 
         return !empty($in_branch);
         break;
     }
   }
 
 }
diff --git a/src/applications/releeph/field/selector/ReleephFieldSelector.php b/src/applications/releeph/field/selector/ReleephFieldSelector.php
index 95f22d0fe..3d554f5d6 100644
--- a/src/applications/releeph/field/selector/ReleephFieldSelector.php
+++ b/src/applications/releeph/field/selector/ReleephFieldSelector.php
@@ -1,48 +1,48 @@
 <?php
 
-abstract class ReleephFieldSelector {
+abstract class ReleephFieldSelector extends Phobject {
 
   final public function __construct() {
     // <empty>
   }
 
   abstract public function getFieldSpecifications();
 
   public function sortFieldsForCommitMessage(array $fields) {
     assert_instances_of($fields, 'ReleephFieldSpecification');
     return $fields;
   }
 
   protected static function selectFields(array $fields, array $classes) {
     assert_instances_of($fields, 'ReleephFieldSpecification');
 
     $map = array();
     foreach ($fields as $field) {
       $map[get_class($field)] = $field;
     }
 
     $result = array();
     foreach ($classes as $class) {
       $field = idx($map, $class);
       if (!$field) {
         throw new Exception(
           pht(
             "Tried to select a in instance of '%s' but that field ".
             "is not configured for this project!",
             $class));
       }
 
       if (idx($result, $class)) {
         throw new Exception(
           pht(
             "You have asked to select the field '%s' more than once!",
             $class));
       }
 
       $result[$class] = $field;
     }
 
     return $result;
   }
 
 }
diff --git a/src/applications/releeph/view/branch/ReleephBranchTemplate.php b/src/applications/releeph/view/branch/ReleephBranchTemplate.php
index 7efaaad9f..f29e7a9fc 100644
--- a/src/applications/releeph/view/branch/ReleephBranchTemplate.php
+++ b/src/applications/releeph/view/branch/ReleephBranchTemplate.php
@@ -1,195 +1,195 @@
 <?php
 
-final class ReleephBranchTemplate {
+final class ReleephBranchTemplate extends Phobject {
 
   const KEY = 'releeph.default-branch-template';
 
+  private $commitHandle;
+  private $branchDate = null;
+  private $projectName;
+  private $isSymbolic;
+
   public static function getDefaultTemplate() {
     return PhabricatorEnv::getEnvConfig(self::KEY);
   }
 
   public static function getRequiredDefaultTemplate() {
     $template = self::getDefaultTemplate();
     if (!$template) {
       throw new Exception(pht(
         "Config setting '%s' must be set, ".
         "or you must provide a branch-template for each project!",
         self::KEY));
     }
     return $template;
   }
 
   public static function getFakeCommitHandleFor(
     $repository_phid,
     PhabricatorUser $viewer) {
 
     $repository = id(new PhabricatorRepositoryQuery())
       ->setViewer($viewer)
       ->withPHIDs(array($repository_phid))
       ->executeOne();
 
     $fake_handle = 'SOFAKE';
     if ($repository) {
       $fake_handle = id(new PhabricatorObjectHandle())
         ->setName($repository->formatCommitName('100000000000'));
     }
     return $fake_handle;
   }
 
-  private $commitHandle;
-  private $branchDate = null;
-  private $projectName;
-  private $isSymbolic;
-
   public function setCommitHandle(PhabricatorObjectHandle $handle) {
     $this->commitHandle = $handle;
     return $this;
   }
 
   public function setBranchDate($branch_date) {
     $this->branchDate = $branch_date;
     return $this;
   }
 
   public function setReleephProjectName($project_name) {
     $this->projectName = $project_name;
     return $this;
   }
 
   public function setSymbolic($is_symbolic) {
     $this->isSymbolic = $is_symbolic;
     return $this;
   }
 
   public function interpolate($template) {
     if (!$this->projectName) {
       return array('', array());
     }
 
     list($name, $name_errors) = $this->interpolateInner(
       $template,
       $this->isSymbolic);
 
     if ($this->isSymbolic) {
       return array($name, $name_errors);
     } else {
       $validate_errors = $this->validateAsBranchName($name);
       $errors = array_merge($name_errors, $validate_errors);
       return array($name, $errors);
     }
   }
 
   /*
    * xsprintf() would be useful here, but that's for formatting concrete lists
    * of things in a certain way...
    *
    *    animal_printf('%A %A %A', $dog1, $dog2, $dog3);
    *
    * ...rather than interpolating percent-control-strings like strftime does.
    */
   private function interpolateInner($template, $is_symbolic) {
     $name = $template;
     $errors = array();
 
     $safe_project_name = str_replace(' ', '-', $this->projectName);
     $short_commit_id = last(
       preg_split('/r[A-Z]+/', $this->commitHandle->getName()));
 
     $interpolations = array();
     for ($ii = 0; $ii < strlen($name); $ii++) {
       $char = substr($name, $ii, 1);
       $prev = null;
       if ($ii > 0) {
         $prev = substr($name, $ii - 1, 1);
       }
       $next = substr($name, $ii + 1, 1);
       if ($next && $char == '%' && $prev != '%') {
         $interpolations[$ii] = $next;
       }
     }
 
     $variable_interpolations = array();
 
     $reverse_interpolations = $interpolations;
     krsort($reverse_interpolations);
 
     if ($this->branchDate) {
       $branch_date = $this->branchDate;
     } else {
       $branch_date = $this->commitHandle->getTimestamp();
     }
 
     foreach ($reverse_interpolations as $position => $code) {
       $replacement = null;
       switch ($code) {
         case 'v':
           $replacement = $this->commitHandle->getName();
           $is_variable = true;
           break;
 
         case 'V':
           $replacement = $short_commit_id;
           $is_variable = true;
           break;
 
         case 'P':
           $replacement = $safe_project_name;
           $is_variable = false;
           break;
 
         case 'p':
           $replacement = strtolower($safe_project_name);
           $is_variable = false;
           break;
 
         default:
           // Format anything else using strftime()
           $replacement = strftime("%{$code}", $branch_date);
           $is_variable = true;
           break;
       }
 
       if ($is_variable) {
         $variable_interpolations[] = $code;
       }
       $name = substr_replace($name, $replacement, $position, 2);
     }
 
     if (!$is_symbolic && !$variable_interpolations) {
       $errors[] = pht("Include additional interpolations that aren't static!");
     }
 
     return array($name, $errors);
   }
 
   private function validateAsBranchName($name) {
     $errors = array();
 
     if (preg_match('{^/}', $name) || preg_match('{/$}', $name)) {
       $errors[] = pht("Branches cannot begin or end with '%s'", '/');
     }
 
     if (preg_match('{//+}', $name)) {
       $errors[] = pht("Branches cannot contain multiple consecutive '%s'", '/');
     }
 
     $parts = array_filter(explode('/', $name));
     foreach ($parts as $index => $part) {
       $part_error = null;
       if (preg_match('{^\.}', $part) || preg_match('{\.$}', $part)) {
         $errors[] = pht("Path components cannot begin or end with '%s'", '.');
       } else if (preg_match('{^(?!\w)}', $part)) {
         $errors[] = pht('Path components must begin with an alphanumeric.');
       } else if (!preg_match('{^\w ([\w-_%\.]* [\w-_%])?$}x', $part)) {
         $errors[] = pht(
           "Path components may only contain alphanumerics ".
           "or '%s', '%s' or '%s'.",
           '-',
           '_',
           '.');
       }
     }
 
     return $errors;
   }
 }
diff --git a/src/applications/repository/constants/PhabricatorRepositoryType.php b/src/applications/repository/constants/PhabricatorRepositoryType.php
index a99c76d88..b3415f446 100644
--- a/src/applications/repository/constants/PhabricatorRepositoryType.php
+++ b/src/applications/repository/constants/PhabricatorRepositoryType.php
@@ -1,24 +1,24 @@
 <?php
 
-final class PhabricatorRepositoryType {
+final class PhabricatorRepositoryType extends Phobject {
 
   const REPOSITORY_TYPE_GIT         = 'git';
   const REPOSITORY_TYPE_SVN         = 'svn';
   const REPOSITORY_TYPE_MERCURIAL   = 'hg';
   const REPOSITORY_TYPE_PERFORCE    = 'p4';
 
   public static function getAllRepositoryTypes() {
     static $map = array(
       self::REPOSITORY_TYPE_GIT       => 'Git',
       self::REPOSITORY_TYPE_SVN       => 'Subversion',
       self::REPOSITORY_TYPE_MERCURIAL => 'Mercurial',
     );
     return $map;
   }
 
   public static function getNameForRepositoryType($type) {
     $map = self::getAllRepositoryTypes();
     return idx($map, $type, pht('Unknown'));
   }
 
 }
diff --git a/src/applications/repository/engine/PhabricatorRepositoryCommitRef.php b/src/applications/repository/engine/PhabricatorRepositoryCommitRef.php
index e257f4fc5..31c859d5d 100644
--- a/src/applications/repository/engine/PhabricatorRepositoryCommitRef.php
+++ b/src/applications/repository/engine/PhabricatorRepositoryCommitRef.php
@@ -1,56 +1,56 @@
 <?php
 
-final class PhabricatorRepositoryCommitRef {
+final class PhabricatorRepositoryCommitRef extends Phobject {
 
   private $identifier;
   private $epoch;
   private $branch;
   private $canCloseImmediately;
   private $parents = array();
 
   public function setIdentifier($identifier) {
     $this->identifier = $identifier;
     return $this;
   }
 
   public function getIdentifier() {
     return $this->identifier;
   }
 
   public function setEpoch($epoch) {
     $this->epoch = $epoch;
     return $this;
   }
 
   public function getEpoch() {
     return $this->epoch;
   }
 
   public function setBranch($branch) {
     $this->branch = $branch;
     return $this;
   }
 
   public function getBranch() {
     return $this->branch;
   }
 
   public function setCanCloseImmediately($can_close_immediately) {
     $this->canCloseImmediately = $can_close_immediately;
     return $this;
   }
 
   public function getCanCloseImmediately() {
     return $this->canCloseImmediately;
   }
 
   public function setParents(array $parents) {
     $this->parents = $parents;
     return $this;
   }
 
   public function getParents() {
     return $this->parents;
   }
 
 }
diff --git a/src/applications/repository/engine/PhabricatorRepositoryEngine.php b/src/applications/repository/engine/PhabricatorRepositoryEngine.php
index d5982473c..0bfe94a0a 100644
--- a/src/applications/repository/engine/PhabricatorRepositoryEngine.php
+++ b/src/applications/repository/engine/PhabricatorRepositoryEngine.php
@@ -1,165 +1,165 @@
 <?php
 
 /**
  * @task config     Configuring Repository Engines
  * @task internal   Internals
  */
-abstract class PhabricatorRepositoryEngine {
+abstract class PhabricatorRepositoryEngine extends Phobject {
 
   private $repository;
   private $verbose;
 
   /**
    * @task config
    */
   public function setRepository(PhabricatorRepository $repository) {
     $this->repository = $repository;
     return $this;
   }
 
 
   /**
    * @task config
    */
   protected function getRepository() {
     if ($this->repository === null) {
       throw new PhutilInvalidStateException('setRepository');
     }
 
     return $this->repository;
   }
 
 
   /**
    * @task config
    */
   public function setVerbose($verbose) {
     $this->verbose = $verbose;
     return $this;
   }
 
 
   /**
    * @task config
    */
   public function getVerbose() {
     return $this->verbose;
   }
 
 
   public function getViewer() {
     return PhabricatorUser::getOmnipotentUser();
   }
 
   /**
    * Verify that the "origin" remote exists, and points at the correct URI.
    *
    * This catches or corrects some types of misconfiguration, and also repairs
    * an issue where Git 1.7.1 does not create an "origin" for `--bare` clones.
    * See T4041.
    *
    * @param   PhabricatorRepository Repository to verify.
    * @return  void
    */
   protected function verifyGitOrigin(PhabricatorRepository $repository) {
     list($remotes) = $repository->execxLocalCommand(
       'remote show -n origin');
 
     $matches = null;
     if (!preg_match('/^\s*Fetch URL:\s*(.*?)\s*$/m', $remotes, $matches)) {
       throw new Exception(
         pht(
           "Expected '%s' in '%s'.",
           'Fetch URL',
           'git remote show -n origin'));
     }
 
     $remote_uri = $matches[1];
     $expect_remote = $repository->getRemoteURI();
 
     if ($remote_uri == 'origin') {
       // If a remote does not exist, git pretends it does and prints out a
       // made up remote where the URI is the same as the remote name. This is
       // definitely not correct.
 
       // Possibly, we should use `git remote --verbose` instead, which does not
       // suffer from this problem (but is a little more complicated to parse).
       $valid = false;
       $exists = false;
     } else {
       $normal_type_git = PhabricatorRepositoryURINormalizer::TYPE_GIT;
 
       $remote_normal = id(new PhabricatorRepositoryURINormalizer(
         $normal_type_git,
         $remote_uri))->getNormalizedPath();
 
       $expect_normal = id(new PhabricatorRepositoryURINormalizer(
         $normal_type_git,
         $expect_remote))->getNormalizedPath();
 
       $valid = ($remote_normal == $expect_normal);
       $exists = true;
     }
 
     if (!$valid) {
       if (!$exists) {
         // If there's no "origin" remote, just create it regardless of how
         // strongly we own the working copy. There is almost no conceivable
         // scenario in which this could do damage.
         $this->log(
           pht(
             'Remote "origin" does not exist. Creating "origin", with '.
             'URI "%s".',
             $expect_remote));
         $repository->execxLocalCommand(
           'remote add origin %P',
           $repository->getRemoteURIEnvelope());
 
         // NOTE: This doesn't fetch the origin (it just creates it), so we won't
         // know about origin branches until the next "pull" happens. That's fine
         // for our purposes, but might impact things in the future.
       } else {
         if ($repository->canDestroyWorkingCopy()) {
           // Bad remote, but we can try to repair it.
           $this->log(
             pht(
               'Remote "origin" exists, but is pointed at the wrong URI, "%s". '.
               'Resetting origin URI to "%s.',
               $remote_uri,
               $expect_remote));
           $repository->execxLocalCommand(
             'remote set-url origin %P',
             $repository->getRemoteURIEnvelope());
         } else {
           // Bad remote and we aren't comfortable repairing it.
           $message = pht(
             'Working copy at "%s" has a mismatched origin URI, "%s". '.
             'The expected origin URI is "%s". Fix your configuration, or '.
             'set the remote URI correctly. To avoid breaking anything, '.
             'Phabricator will not automatically fix this.',
             $repository->getLocalPath(),
             $remote_uri,
             $expect_remote);
           throw new Exception($message);
         }
       }
     }
   }
 
 
 
 
   /**
    * @task internal
    */
   protected function log($pattern /* ... */) {
     if ($this->getVerbose()) {
       $console = PhutilConsole::getConsole();
       $argv = func_get_args();
       array_unshift($argv, "%s\n");
       call_user_func_array(array($console, 'writeOut'), $argv);
     }
     return $this;
   }
 
 }
diff --git a/src/applications/repository/graphcache/PhabricatorRepositoryGraphCache.php b/src/applications/repository/graphcache/PhabricatorRepositoryGraphCache.php
index 5c3081a7b..302006309 100644
--- a/src/applications/repository/graphcache/PhabricatorRepositoryGraphCache.php
+++ b/src/applications/repository/graphcache/PhabricatorRepositoryGraphCache.php
@@ -1,409 +1,409 @@
 <?php
 
 /**
  * Given a commit and a path, efficiently determine the most recent ancestor
  * commit where the path was touched.
  *
  * In Git and Mercurial, log operations with a path are relatively slow. For
  * example:
  *
  *    git log -n1 <commit> -- <path>
  *
  * ...routinely takes several hundred milliseconds, and equivalent requests
  * often take longer in Mercurial.
  *
  * Unfortunately, this operation is fundamental to rendering a repository for
  * the web, and essentially everything else that's slow can be reduced to this
  * plus some trivial work afterward. Making this fast is desirable and powerful,
  * and allows us to make other things fast by expressing them in terms of this
  * query.
  *
  * Because the query is fundamentally a graph query, it isn't easy to express
  * in a reasonable way in MySQL, and we can't do round trips to the server to
  * walk the graph without incurring huge performance penalties.
  *
  * However, the total amount of data in the graph is relatively small. By
  * caching it in chunks and keeping it in APC, we can reasonably load and walk
  * the graph in PHP quickly.
  *
  * For more context, see T2683.
  *
  * Structure of the Cache
  * ======================
  *
  * The cache divides commits into buckets (see @{method:getBucketSize}). To
  * walk the graph, we pull a commit's bucket. The bucket is a map from commit
  * IDs to a list of parents and changed paths, separated by `null`. For
  * example, a bucket might look like this:
  *
  *   array(
  *     1 => array(0, null, 17, 18),
  *     2 => array(1, null, 4),
  *     // ...
  *   )
  *
  * This means that commit ID 1 has parent commit 0 (a special value meaning
  * no parents) and affected path IDs 17 and 18. Commit ID 2 has parent commit 1,
  * and affected path 4.
  *
  * This data structure attempts to balance compactness, ease of construction,
  * simplicity of cache semantics, and lookup performance. In the average case,
  * it appears to do a reasonable job at this.
  *
  * @task query Querying the Graph Cache
  * @task cache Cache Internals
  */
-final class PhabricatorRepositoryGraphCache {
+final class PhabricatorRepositoryGraphCache extends Phobject {
 
   private $rebuiltKeys = array();
 
 
 /* -(  Querying the Graph Cache  )------------------------------------------- */
 
 
   /**
    * Search the graph cache for the most modification to a path.
    *
    * @param int     The commit ID to search ancestors of.
    * @param int     The path ID to search for changes to.
    * @param float   Maximum number of seconds to spend trying to satisfy this
    *                query using the graph cache. By default, `0.5` (500ms).
    * @return mixed  Commit ID, or `null` if no ancestors exist, or `false` if
    *                the graph cache was unable to determine the answer.
    * @task query
    */
   public function loadLastModifiedCommitID($commit_id, $path_id, $time = 0.5) {
     $commit_id = (int)$commit_id;
     $path_id = (int)$path_id;
 
     $bucket_data = null;
     $data_key = null;
     $seen = array();
 
     $t_start = microtime(true);
     $iterations = 0;
     while (true) {
       $bucket_key = $this->getBucketKey($commit_id);
 
       if (($data_key != $bucket_key) || $bucket_data === null) {
         $bucket_data = $this->getBucketData($bucket_key);
         $data_key = $bucket_key;
       }
 
       if (empty($bucket_data[$commit_id])) {
         // Rebuild the cache bucket, since the commit might be a very recent
         // one that we'll pick up by rebuilding.
 
         $bucket_data = $this->getBucketData($bucket_key, $bucket_data);
         if (empty($bucket_data[$commit_id])) {
           // A rebuild didn't help. This can occur legitimately if the commit
           // is new and hasn't parsed yet.
           return false;
         }
 
         // Otherwise, the rebuild gave us the data, so we can keep going.
       }
 
       // Sanity check so we can survive and recover from bad data.
       if (isset($seen[$commit_id])) {
         phlog(pht('Unexpected infinite loop in %s!', __CLASS__));
         return false;
       } else {
         $seen[$commit_id] = true;
       }
 
       // `$data` is a list: the commit's parent IDs, followed by `null`,
       // followed by the modified paths in ascending order. We figure out the
       // first parent first, then check if the path was touched. If the path
       // was touched, this is the commit we're after. If not, walk backward
       // in the tree.
 
       $items = $bucket_data[$commit_id];
       $size = count($items);
 
       // Walk past the parent information.
       $parent_id = null;
       for ($ii = 0;; ++$ii) {
         if ($items[$ii] === null) {
           break;
         }
         if ($parent_id === null) {
           $parent_id = $items[$ii];
         }
       }
 
       // Look for a modification to the path.
       for (; $ii < $size; ++$ii) {
         $item = $items[$ii];
         if ($item > $path_id) {
           break;
         }
         if ($item === $path_id) {
           return $commit_id;
         }
       }
 
       if ($parent_id) {
         $commit_id = $parent_id;
 
         // Periodically check if we've spent too long looking for a result
         // in the cache, and return so we can fall back to a VCS operation. This
         // keeps us from having a degenerate worst case if, e.g., the cache
         // is cold and we need to inspect a very large number of blocks
         // to satisfy the query.
 
         if (((++$iterations) % 64) === 0) {
           $t_end = microtime(true);
           if (($t_end - $t_start) > $time) {
             return false;
           }
         }
         continue;
       }
 
       // If we have an explicit 0, that means this commit really has no parents.
       // Usually, it is the first commit in the repository.
       if ($parent_id === 0) {
         return null;
       }
 
       // If we didn't find a parent, the parent data isn't available. We fail
       // to find an answer in the cache and fall back to querying the VCS.
       return false;
     }
   }
 
 
 /* -(  Cache Internals  )---------------------------------------------------- */
 
 
   /**
    * Get the bucket key for a given commit ID.
    *
    * @param   int   Commit ID.
    * @return  int   Bucket key.
    * @task cache
    */
   private function getBucketKey($commit_id) {
     return (int)floor($commit_id / $this->getBucketSize());
   }
 
 
   /**
    * Get the cache key for a given bucket key (from @{method:getBucketKey}).
    *
    * @param   int     Bucket key.
    * @return  string  Cache key.
    * @task cache
    */
   private function getBucketCacheKey($bucket_key) {
     static $prefix;
 
     if ($prefix === null) {
       $self = get_class($this);
       $size = $this->getBucketSize();
       $prefix = "{$self}:{$size}:2:";
     }
 
     return $prefix.$bucket_key;
   }
 
 
   /**
    * Get the number of items per bucket.
    *
    * @return  int Number of items to store per bucket.
    * @task cache
    */
   private function getBucketSize() {
     return 4096;
   }
 
 
   /**
    * Retrieve or build a graph cache bucket from the cache.
    *
    * Normally, this operates as a readthrough cache call. It can also be used
    * to force a cache update by passing the existing data to `$rebuild_data`.
    *
    * @param   int     Bucket key, from @{method:getBucketKey}.
    * @param   mixed   Current data, to force a cache rebuild of this bucket.
    * @return  array   Data from the cache.
    * @task cache
    */
   private function getBucketData($bucket_key, $rebuild_data = null) {
     $cache_key = $this->getBucketCacheKey($bucket_key);
 
     // TODO: This cache stuff could be handled more gracefully, but the
     // database cache currently requires values to be strings and needs
     // some tweaking to support this as part of a stack. Our cache semantics
     // here are also unusual (not purely readthrough) because this cache is
     // appendable.
 
     $cache_level1 = PhabricatorCaches::getRepositoryGraphL1Cache();
     $cache_level2 = PhabricatorCaches::getRepositoryGraphL2Cache();
     if ($rebuild_data === null) {
       $bucket_data = $cache_level1->getKey($cache_key);
       if ($bucket_data) {
         return $bucket_data;
       }
 
       $bucket_data = $cache_level2->getKey($cache_key);
       if ($bucket_data) {
         $unserialized = @unserialize($bucket_data);
         if ($unserialized) {
           // Fill APC if we got a database hit but missed in APC.
           $cache_level1->setKey($cache_key, $unserialized);
           return $unserialized;
         }
       }
     }
 
     if (!is_array($rebuild_data)) {
       $rebuild_data = array();
     }
 
     $bucket_data = $this->rebuildBucket($bucket_key, $rebuild_data);
 
     // Don't bother writing the data if we didn't update anything.
     if ($bucket_data !== $rebuild_data) {
       $cache_level2->setKey($cache_key, serialize($bucket_data));
       $cache_level1->setKey($cache_key, $bucket_data);
     }
 
     return $bucket_data;
   }
 
 
   /**
    * Rebuild a cache bucket, amending existing data if available.
    *
    * @param   int     Bucket key, from @{method:getBucketKey}.
    * @param   array   Existing bucket data.
    * @return  array   Rebuilt bucket data.
    * @task cache
    */
   private function rebuildBucket($bucket_key, array $current_data) {
 
     // First, check if we've already rebuilt this bucket. In some cases (like
     // browsing a repository at some commit) it's common to issue many lookups
     // against one commit. If that commit has been discovered but not yet
     // fully imported, we'll repeatedly attempt to rebuild the bucket. If the
     // first rebuild did not work, subsequent rebuilds are very unlikely to
     // have any effect. We can just skip the rebuild in these cases.
 
     if (isset($this->rebuiltKeys[$bucket_key])) {
       return $current_data;
     } else {
       $this->rebuiltKeys[$bucket_key] = true;
     }
 
     $bucket_min = ($bucket_key * $this->getBucketSize());
     $bucket_max = ($bucket_min + $this->getBucketSize()) - 1;
 
     // We need to reload all of the commits in the bucket because there is
     // no guarantee that they'll get parsed in order, so we can fill large
     // commit IDs before small ones. Later on, we'll ignore the commits we
     // already know about.
 
     $table_commit = new PhabricatorRepositoryCommit();
     $table_repository = new PhabricatorRepository();
     $conn_r = $table_commit->establishConnection('r');
 
     // Find all the Git and Mercurial commits in the block which have completed
     // change import. We can't fill the cache accurately for commits which have
     // not completed change import, so just pretend we don't know about them.
     // In these cases, we will will ultimately fall back to VCS queries.
 
     $commit_rows = queryfx_all(
       $conn_r,
       'SELECT c.id FROM %T c
         JOIN %T r ON c.repositoryID = r.id AND r.versionControlSystem IN (%Ls)
         WHERE c.id BETWEEN %d AND %d
           AND (c.importStatus & %d) = %d',
       $table_commit->getTableName(),
       $table_repository->getTableName(),
       array(
         PhabricatorRepositoryType::REPOSITORY_TYPE_GIT,
         PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL,
       ),
       $bucket_min,
       $bucket_max,
       PhabricatorRepositoryCommit::IMPORTED_CHANGE,
       PhabricatorRepositoryCommit::IMPORTED_CHANGE);
 
     // If we don't have any data, just return the existing data.
     if (!$commit_rows) {
       return $current_data;
     }
 
     // Remove the commits we already have data for. We don't need to rebuild
     // these. If there's nothing left, return the existing data.
 
     $commit_ids = ipull($commit_rows, 'id', 'id');
     $commit_ids = array_diff_key($commit_ids, $current_data);
 
     if (!$commit_ids) {
       return $current_data;
     }
 
     // Find all the path changes for the new commits.
     $path_changes = queryfx_all(
       $conn_r,
       'SELECT commitID, pathID FROM %T
         WHERE commitID IN (%Ld)
         AND (isDirect = 1 OR changeType = %d)',
       PhabricatorRepository::TABLE_PATHCHANGE,
       $commit_ids,
       DifferentialChangeType::TYPE_CHILD);
     $path_changes = igroup($path_changes, 'commitID');
 
     // Find all the parents for the new commits.
     $parents = queryfx_all(
       $conn_r,
       'SELECT childCommitID, parentCommitID FROM %T
         WHERE childCommitID IN (%Ld)
         ORDER BY id ASC',
       PhabricatorRepository::TABLE_PARENTS,
       $commit_ids);
     $parents = igroup($parents, 'childCommitID');
 
     // Build the actual data for the cache.
     foreach ($commit_ids as $commit_id) {
       $parent_ids = array();
       if (!empty($parents[$commit_id])) {
         foreach ($parents[$commit_id] as $row) {
           $parent_ids[] = (int)$row['parentCommitID'];
         }
       } else {
         // We expect all rows to have parents (commits with no parents get
         // an explicit "0" placeholder). If we're in an older repository, the
         // parent information might not have been populated yet. Decline to fill
         // the cache if we don't have the parent information, since the fill
         // will be incorrect.
         continue;
       }
 
       if (isset($path_changes[$commit_id])) {
         $path_ids = $path_changes[$commit_id];
         foreach ($path_ids as $key => $path_id) {
           $path_ids[$key] = (int)$path_id['pathID'];
         }
         sort($path_ids);
       } else {
         $path_ids = array();
       }
 
       $value = $parent_ids;
       $value[] = null;
       foreach ($path_ids as $path_id) {
         $value[] = $path_id;
       }
 
       $current_data[$commit_id] = $value;
     }
 
     return $current_data;
   }
 
 }
diff --git a/src/applications/repository/storage/__tests__/PhabricatorRepositoryURITestCase.php b/src/applications/repository/storage/__tests__/PhabricatorRepositoryURITestCase.php
index d3535102d..0d13cf0e0 100644
--- a/src/applications/repository/storage/__tests__/PhabricatorRepositoryURITestCase.php
+++ b/src/applications/repository/storage/__tests__/PhabricatorRepositoryURITestCase.php
@@ -1,108 +1,108 @@
 <?php
 
 final class PhabricatorRepositoryURITestCase
   extends PhabricatorTestCase {
 
   protected function getPhabricatorTestCaseConfiguration() {
     return array(
       self::PHABRICATOR_TESTCONFIG_BUILD_STORAGE_FIXTURES => true,
     );
   }
 
   public function testURIGeneration() {
     $svn = PhabricatorRepositoryType::REPOSITORY_TYPE_SVN;
     $git = PhabricatorRepositoryType::REPOSITORY_TYPE_GIT;
     $hg = PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL;
 
     $user = $this->generateNewTestUser();
 
     $http_secret = id(new PassphraseSecret())->setSecretData('quack')->save();
 
     $http_credential = PassphraseCredential::initializeNewCredential($user)
-      ->setCredentialType(PassphraseCredentialTypePassword::CREDENTIAL_TYPE)
-      ->setProvidesType(PassphraseCredentialTypePassword::PROVIDES_TYPE)
+      ->setCredentialType(PassphrasePasswordCredentialType::CREDENTIAL_TYPE)
+      ->setProvidesType(PassphrasePasswordCredentialType::PROVIDES_TYPE)
       ->setUsername('duck')
       ->setSecretID($http_secret->getID())
       ->save();
 
     $repo = PhabricatorRepository::initializeNewRepository($user)
       ->setVersionControlSystem($svn)
       ->setName(pht('Test Repo'))
       ->setCallsign('TESTREPO')
       ->setCredentialPHID($http_credential->getPHID())
       ->save();
 
     // Test HTTP URIs.
 
     $repo->setDetail('remote-uri', 'http://example.com/');
     $repo->setVersionControlSystem($svn);
 
     $this->assertEqual('http://example.com/', $repo->getRemoteURI());
     $this->assertEqual('http://example.com/', $repo->getPublicCloneURI());
     $this->assertEqual('http://example.com/',
       $repo->getRemoteURIEnvelope()->openEnvelope());
 
     $repo->setVersionControlSystem($git);
 
     $this->assertEqual('http://example.com/', $repo->getRemoteURI());
     $this->assertEqual('http://example.com/', $repo->getPublicCloneURI());
     $this->assertEqual('http://duck:quack@example.com/',
       $repo->getRemoteURIEnvelope()->openEnvelope());
 
     $repo->setVersionControlSystem($hg);
 
     $this->assertEqual('http://example.com/', $repo->getRemoteURI());
     $this->assertEqual('http://example.com/', $repo->getPublicCloneURI());
     $this->assertEqual('http://duck:quack@example.com/',
       $repo->getRemoteURIEnvelope()->openEnvelope());
 
     // Test SSH URIs.
 
     $repo->setDetail('remote-uri', 'ssh://example.com/');
     $repo->setVersionControlSystem($svn);
 
     $this->assertEqual('ssh://example.com/', $repo->getRemoteURI());
     $this->assertEqual('ssh://example.com/', $repo->getPublicCloneURI());
     $this->assertEqual('ssh://example.com/',
       $repo->getRemoteURIEnvelope()->openEnvelope());
 
     $repo->setVersionControlSystem($git);
 
     $this->assertEqual('ssh://example.com/', $repo->getRemoteURI());
     $this->assertEqual('ssh://example.com/', $repo->getPublicCloneURI());
     $this->assertEqual('ssh://example.com/',
       $repo->getRemoteURIEnvelope()->openEnvelope());
 
     $repo->setVersionControlSystem($hg);
 
     $this->assertEqual('ssh://example.com/', $repo->getRemoteURI());
     $this->assertEqual('ssh://example.com/', $repo->getPublicCloneURI());
     $this->assertEqual('ssh://example.com/',
       $repo->getRemoteURIEnvelope()->openEnvelope());
 
     // Test Git URIs.
 
     $repo->setDetail('remote-uri', 'git@example.com:path.git');
     $repo->setVersionControlSystem($git);
 
     $this->assertEqual('git@example.com:path.git', $repo->getRemoteURI());
     $this->assertEqual('git@example.com:path.git', $repo->getPublicCloneURI());
     $this->assertEqual('git@example.com:path.git',
       $repo->getRemoteURIEnvelope()->openEnvelope());
 
     // Test SVN "Import Only" paths.
 
     $repo->setDetail('remote-uri', 'http://example.com/');
     $repo->setVersionControlSystem($svn);
     $repo->setDetail('svn-subpath', 'projects/example/');
 
     $this->assertEqual('http://example.com/', $repo->getRemoteURI());
     $this->assertEqual(
       'http://example.com/projects/example/',
       $repo->getPublicCloneURI());
     $this->assertEqual('http://example.com/',
       $repo->getRemoteURIEnvelope()->openEnvelope());
 
   }
 
 }
diff --git a/src/applications/search/constants/PhabricatorSearchRelationship.php b/src/applications/search/constants/PhabricatorSearchRelationship.php
index 8631aeec2..0d43f0bdc 100644
--- a/src/applications/search/constants/PhabricatorSearchRelationship.php
+++ b/src/applications/search/constants/PhabricatorSearchRelationship.php
@@ -1,18 +1,18 @@
 <?php
 
-final class PhabricatorSearchRelationship {
+final class PhabricatorSearchRelationship extends Phobject {
 
   const RELATIONSHIP_AUTHOR     = 'auth';
   const RELATIONSHIP_BOOK       = 'book';
   const RELATIONSHIP_REVIEWER   = 'revw';
   const RELATIONSHIP_SUBSCRIBER = 'subs';
   const RELATIONSHIP_COMMENTER  = 'comm';
   const RELATIONSHIP_OWNER      = 'ownr';
   const RELATIONSHIP_PROJECT    = 'proj';
   const RELATIONSHIP_REPOSITORY = 'repo';
 
   const RELATIONSHIP_OPEN       = 'open';
   const RELATIONSHIP_CLOSED     = 'clos';
   const RELATIONSHIP_UNOWNED    = 'unow';
 
 }
diff --git a/src/applications/search/engine/PhabricatorApplicationSearchEngine.php b/src/applications/search/engine/PhabricatorApplicationSearchEngine.php
index eeb2d0c0e..46de71134 100644
--- a/src/applications/search/engine/PhabricatorApplicationSearchEngine.php
+++ b/src/applications/search/engine/PhabricatorApplicationSearchEngine.php
@@ -1,1127 +1,1131 @@
 <?php
 
 /**
  * Represents an abstract search engine for an application. It supports
  * creating and storing saved queries.
  *
  * @task construct  Constructing Engines
  * @task app        Applications
  * @task builtin    Builtin Queries
  * @task uri        Query URIs
  * @task dates      Date Filters
  * @task order      Result Ordering
  * @task read       Reading Utilities
  * @task exec       Paging and Executing Queries
  * @task render     Rendering Results
  */
 abstract class PhabricatorApplicationSearchEngine extends Phobject {
 
   private $application;
   private $viewer;
   private $errors = array();
   private $customFields = false;
   private $request;
   private $context;
 
   const CONTEXT_LIST  = 'list';
   const CONTEXT_PANEL = 'panel';
 
   public function newResultObject() {
     // We may be able to get this automatically if newQuery() is implemented.
     $query = $this->newQuery();
     if ($query) {
       $object = $query->newResultObject();
       if ($object) {
         return $object;
       }
     }
 
     return null;
   }
 
   public function newQuery() {
     return null;
   }
 
   public function setViewer(PhabricatorUser $viewer) {
     $this->viewer = $viewer;
     return $this;
   }
 
   protected function requireViewer() {
     if (!$this->viewer) {
       throw new PhutilInvalidStateException('setViewer');
     }
     return $this->viewer;
   }
 
   public function setContext($context) {
     $this->context = $context;
     return $this;
   }
 
   public function isPanelContext() {
     return ($this->context == self::CONTEXT_PANEL);
   }
 
   public function canUseInPanelContext() {
     return true;
   }
 
   public function saveQuery(PhabricatorSavedQuery $query) {
     $query->setEngineClassName(get_class($this));
 
     $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
     try {
       $query->save();
     } catch (AphrontDuplicateKeyQueryException $ex) {
       // Ignore, this is just a repeated search.
     }
     unset($unguarded);
   }
 
   /**
    * Create a saved query object from the request.
    *
    * @param AphrontRequest The search request.
    * @return PhabricatorSavedQuery
    */
   public function buildSavedQueryFromRequest(AphrontRequest $request) {
     $fields = $this->buildSearchFields();
     $viewer = $this->requireViewer();
 
     $saved = new PhabricatorSavedQuery();
     foreach ($fields as $field) {
       $field->setViewer($viewer);
 
       $value = $field->readValueFromRequest($request);
       $saved->setParameter($field->getKey(), $value);
     }
 
     return $saved;
   }
 
   /**
    * Executes the saved query.
    *
    * @param PhabricatorSavedQuery The saved query to operate on.
    * @return The result of the query.
    */
   public function buildQueryFromSavedQuery(PhabricatorSavedQuery $saved) {
     $saved = clone $saved;
     $this->willUseSavedQuery($saved);
 
     $fields = $this->buildSearchFields();
     $viewer = $this->requireViewer();
 
     $map = array();
     foreach ($fields as $field) {
       $field->setViewer($viewer);
       $field->readValueFromSavedQuery($saved);
       $value = $field->getValueForQuery($field->getValue());
       $map[$field->getKey()] = $value;
     }
 
     $query = $this->buildQueryFromParameters($map);
 
     $object = $this->newResultObject();
     if (!$object) {
       return $query;
     }
 
     if ($object instanceof PhabricatorSubscribableInterface) {
       if (!empty($map['subscriberPHIDs'])) {
         $query->withEdgeLogicPHIDs(
           PhabricatorObjectHasSubscriberEdgeType::EDGECONST,
           PhabricatorQueryConstraint::OPERATOR_OR,
           $map['subscriberPHIDs']);
       }
     }
 
     if ($object instanceof PhabricatorProjectInterface) {
       if (!empty($map['projectPHIDs'])) {
         $query->withEdgeLogicConstraints(
           PhabricatorProjectObjectHasProjectEdgeType::EDGECONST,
           $map['projectPHIDs']);
       }
     }
 
     if ($object instanceof PhabricatorSpacesInterface) {
       if (!empty($map['spacePHIDs'])) {
         $query->withSpacePHIDs($map['spacePHIDs']);
+      } else {
+        // If the user doesn't search for objects in specific spaces, we
+        // default to "all active spaces you have permission to view".
+        $query->withSpaceIsArchived(false);
       }
     }
 
     if ($object instanceof PhabricatorCustomFieldInterface) {
       $this->applyCustomFieldsToQuery($query, $saved);
     }
 
     $order = $saved->getParameter('order');
     $builtin = $query->getBuiltinOrderAliasMap();
     if (strlen($order) && isset($builtin[$order])) {
       $query->setOrder($order);
     } else {
       // If the order is invalid or not available, we choose the first
       // builtin order. This isn't always the default order for the query,
       // but is the first value in the "Order" dropdown, and makes the query
       // behavior more consistent with the UI. In queries where the two
       // orders differ, this order is the preferred order for humans.
       $query->setOrder(head_key($builtin));
     }
 
     return $query;
   }
 
   /**
    * Hook for subclasses to adjust saved queries prior to use.
    *
    * If an application changes how queries are saved, it can implement this
    * hook to keep old queries working the way users expect, by reading,
    * adjusting, and overwriting parameters.
    *
    * @param PhabricatorSavedQuery Saved query which will be executed.
    * @return void
    */
   protected function willUseSavedQuery(PhabricatorSavedQuery $saved) {
     return;
   }
 
   protected function buildQueryFromParameters(array $parameters) {
     throw new PhutilMethodNotImplementedException();
   }
 
   /**
    * Builds the search form using the request.
    *
    * @param AphrontFormView       Form to populate.
    * @param PhabricatorSavedQuery The query from which to build the form.
    * @return void
    */
   public function buildSearchForm(
     AphrontFormView $form,
     PhabricatorSavedQuery $saved) {
 
     $saved = clone $saved;
     $this->willUseSavedQuery($saved);
 
     $fields = $this->buildSearchFields();
     $fields = $this->adjustFieldsForDisplay($fields);
     $viewer = $this->requireViewer();
 
     foreach ($fields as $field) {
       $field->setViewer($viewer);
       $field->readValueFromSavedQuery($saved);
     }
 
     foreach ($fields as $field) {
       foreach ($field->getErrors() as $error) {
         $this->addError(last($error));
       }
     }
 
     foreach ($fields as $field) {
       $field->appendToForm($form);
     }
   }
 
   protected function buildSearchFields() {
     $fields = array();
 
     foreach ($this->buildCustomSearchFields() as $field) {
       $fields[] = $field;
     }
 
     $object = $this->newResultObject();
     if ($object) {
       if ($object instanceof PhabricatorSubscribableInterface) {
         $fields[] = id(new PhabricatorSearchSubscribersField())
           ->setLabel(pht('Subscribers'))
           ->setKey('subscriberPHIDs')
           ->setAliases(array('subscriber', 'subscribers'));
       }
 
       if ($object instanceof PhabricatorProjectInterface) {
         $fields[] = id(new PhabricatorSearchProjectsField())
           ->setKey('projectPHIDs')
           ->setAliases(array('project', 'projects'))
           ->setLabel(pht('Projects'));
       }
 
       if ($object instanceof PhabricatorSpacesInterface) {
         if (PhabricatorSpacesNamespaceQuery::getSpacesExist()) {
           $fields[] = id(new PhabricatorSearchSpacesField())
             ->setKey('spacePHIDs')
             ->setAliases(array('space', 'spaces'))
             ->setLabel(pht('Spaces'));
         }
       }
     }
 
     foreach ($this->buildCustomFieldSearchFields() as $custom_field) {
       $fields[] = $custom_field;
     }
 
     $query = $this->newQuery();
     if ($query) {
       $orders = $query->getBuiltinOrders();
       $orders = ipull($orders, 'name');
 
       $fields[] = id(new PhabricatorSearchOrderField())
         ->setLabel(pht('Order By'))
         ->setKey('order')
         ->setOrderAliases($query->getBuiltinOrderAliasMap())
         ->setOptions($orders);
     }
 
     $field_map = array();
     foreach ($fields as $field) {
       $key = $field->getKey();
       if (isset($field_map[$key])) {
         throw new Exception(
           pht(
             'Two fields in this SearchEngine use the same key ("%s"), but '.
             'each field must use a unique key.',
             $key));
       }
       $field_map[$key] = $field;
     }
 
     return $field_map;
   }
 
   private function adjustFieldsForDisplay(array $field_map) {
     $order = $this->getDefaultFieldOrder();
 
     $head_keys = array();
     $tail_keys = array();
     $seen_tail = false;
     foreach ($order as $order_key) {
       if ($order_key === '...') {
         $seen_tail = true;
         continue;
       }
 
       if (!$seen_tail) {
         $head_keys[] = $order_key;
       } else {
         $tail_keys[] = $order_key;
       }
     }
 
     $head = array_select_keys($field_map, $head_keys);
     $body = array_diff_key($field_map, array_fuse($tail_keys));
     $tail = array_select_keys($field_map, $tail_keys);
 
     $result = $head + $body + $tail;
 
     foreach ($this->getHiddenFields() as $hidden_key) {
       unset($result[$hidden_key]);
     }
 
     return $result;
   }
 
   protected function buildCustomSearchFields() {
     throw new PhutilMethodNotImplementedException();
   }
 
 
   /**
    * Define the default display order for fields by returning a list of
    * field keys.
    *
    * You can use the special key `...` to mean "all unspecified fields go
    * here". This lets you easily put important fields at the top of the form,
    * standard fields in the middle of the form, and less important fields at
    * the bottom.
    *
    * For example, you might return a list like this:
    *
    *   return array(
    *     'authorPHIDs',
    *     'reviewerPHIDs',
    *     '...',
    *     'createdAfter',
    *     'createdBefore',
    *   );
    *
    * Any unspecified fields (including custom fields and fields added
    * automatically by infrastruture) will be put in the middle.
    *
    * @return list<string> Default ordering for field keys.
    */
   protected function getDefaultFieldOrder() {
     return array();
   }
 
   /**
    * Return a list of field keys which should be hidden from the viewer.
    *
     * @return list<string> Fields to hide.
    */
   protected function getHiddenFields() {
     return array();
   }
 
   public function getErrors() {
     return $this->errors;
   }
 
   public function addError($error) {
     $this->errors[] = $error;
     return $this;
   }
 
   /**
    * Return an application URI corresponding to the results page of a query.
    * Normally, this is something like `/application/query/QUERYKEY/`.
    *
    * @param   string  The query key to build a URI for.
    * @return  string  URI where the query can be executed.
    * @task uri
    */
   public function getQueryResultsPageURI($query_key) {
     return $this->getURI('query/'.$query_key.'/');
   }
 
 
   /**
    * Return an application URI for query management. This is used when, e.g.,
    * a query deletion operation is cancelled.
    *
    * @return  string  URI where queries can be managed.
    * @task uri
    */
   public function getQueryManagementURI() {
     return $this->getURI('query/edit/');
   }
 
 
   /**
    * Return the URI to a path within the application. Used to construct default
    * URIs for management and results.
    *
    * @return string URI to path.
    * @task uri
    */
   abstract protected function getURI($path);
 
 
   /**
    * Return a human readable description of the type of objects this query
    * searches for.
    *
    * For example, "Tasks" or "Commits".
    *
    * @return string Human-readable description of what this engine is used to
    *   find.
    */
   abstract public function getResultTypeDescription();
 
 
   public function newSavedQuery() {
     return id(new PhabricatorSavedQuery())
       ->setEngineClassName(get_class($this));
   }
 
   public function addNavigationItems(PHUIListView $menu) {
     $viewer = $this->requireViewer();
 
     $menu->newLabel(pht('Queries'));
 
     $named_queries = $this->loadEnabledNamedQueries();
 
     foreach ($named_queries as $query) {
       $key = $query->getQueryKey();
       $uri = $this->getQueryResultsPageURI($key);
       $menu->newLink($query->getQueryName(), $uri, 'query/'.$key);
     }
 
     if ($viewer->isLoggedIn()) {
       $manage_uri = $this->getQueryManagementURI();
       $menu->newLink(pht('Edit Queries...'), $manage_uri, 'query/edit');
     }
 
     $menu->newLabel(pht('Search'));
     $advanced_uri = $this->getQueryResultsPageURI('advanced');
     $menu->newLink(pht('Advanced Search'), $advanced_uri, 'query/advanced');
 
     return $this;
   }
 
   public function loadAllNamedQueries() {
     $viewer = $this->requireViewer();
 
     $named_queries = id(new PhabricatorNamedQueryQuery())
       ->setViewer($viewer)
       ->withUserPHIDs(array($viewer->getPHID()))
       ->withEngineClassNames(array(get_class($this)))
       ->execute();
     $named_queries = mpull($named_queries, null, 'getQueryKey');
 
     $builtin = $this->getBuiltinQueries($viewer);
     $builtin = mpull($builtin, null, 'getQueryKey');
 
     foreach ($named_queries as $key => $named_query) {
       if ($named_query->getIsBuiltin()) {
         if (isset($builtin[$key])) {
           $named_queries[$key]->setQueryName($builtin[$key]->getQueryName());
           unset($builtin[$key]);
         } else {
           unset($named_queries[$key]);
         }
       }
 
       unset($builtin[$key]);
     }
 
     $named_queries = msort($named_queries, 'getSortKey');
 
     return $named_queries + $builtin;
   }
 
   public function loadEnabledNamedQueries() {
     $named_queries = $this->loadAllNamedQueries();
     foreach ($named_queries as $key => $named_query) {
       if ($named_query->getIsBuiltin() && $named_query->getIsDisabled()) {
         unset($named_queries[$key]);
       }
     }
     return $named_queries;
   }
 
   protected function setQueryProjects(
     PhabricatorCursorPagedPolicyAwareQuery $query,
     PhabricatorSavedQuery $saved) {
 
     $datasource = id(new PhabricatorProjectLogicalDatasource())
       ->setViewer($this->requireViewer());
 
     $projects = $saved->getParameter('projects', array());
     $constraints = $datasource->evaluateTokens($projects);
 
     if ($constraints) {
       $query->withEdgeLogicConstraints(
         PhabricatorProjectObjectHasProjectEdgeType::EDGECONST,
         $constraints);
     }
   }
 
 
 /* -(  Applications  )------------------------------------------------------- */
 
 
   protected function getApplicationURI($path = '') {
     return $this->getApplication()->getApplicationURI($path);
   }
 
   protected function getApplication() {
     if (!$this->application) {
       $class = $this->getApplicationClassName();
 
       $this->application = id(new PhabricatorApplicationQuery())
         ->setViewer($this->requireViewer())
         ->withClasses(array($class))
         ->withInstalled(true)
         ->executeOne();
 
       if (!$this->application) {
         throw new Exception(
           pht(
             'Application "%s" is not installed!',
             $class));
       }
     }
 
     return $this->application;
   }
 
   abstract public function getApplicationClassName();
 
 
 /* -(  Constructing Engines  )----------------------------------------------- */
 
 
   /**
    * Load all available application search engines.
    *
    * @return list<PhabricatorApplicationSearchEngine> All available engines.
    * @task construct
    */
   public static function getAllEngines() {
     $engines = id(new PhutilSymbolLoader())
       ->setAncestorClass(__CLASS__)
       ->loadObjects();
 
     return $engines;
   }
 
 
   /**
    * Get an engine by class name, if it exists.
    *
    * @return PhabricatorApplicationSearchEngine|null Engine, or null if it does
    *   not exist.
    * @task construct
    */
   public static function getEngineByClassName($class_name) {
     return idx(self::getAllEngines(), $class_name);
   }
 
 
 /* -(  Builtin Queries  )---------------------------------------------------- */
 
 
   /**
    * @task builtin
    */
   public function getBuiltinQueries() {
     $names = $this->getBuiltinQueryNames();
 
     $queries = array();
     $sequence = 0;
     foreach ($names as $key => $name) {
       $queries[$key] = id(new PhabricatorNamedQuery())
         ->setUserPHID($this->requireViewer()->getPHID())
         ->setEngineClassName(get_class($this))
         ->setQueryName($name)
         ->setQueryKey($key)
         ->setSequence((1 << 24) + $sequence++)
         ->setIsBuiltin(true);
     }
 
     return $queries;
   }
 
 
   /**
    * @task builtin
    */
   public function getBuiltinQuery($query_key) {
     if (!$this->isBuiltinQuery($query_key)) {
       throw new Exception(pht("'%s' is not a builtin!", $query_key));
     }
     return idx($this->getBuiltinQueries(), $query_key);
   }
 
 
   /**
    * @task builtin
    */
   protected function getBuiltinQueryNames() {
     return array();
   }
 
 
   /**
    * @task builtin
    */
   public function isBuiltinQuery($query_key) {
     $builtins = $this->getBuiltinQueries();
     return isset($builtins[$query_key]);
   }
 
 
   /**
    * @task builtin
    */
   public function buildSavedQueryFromBuiltin($query_key) {
     throw new Exception(pht("Builtin '%s' is not supported!", $query_key));
   }
 
 
 /* -(  Reading Utilities )--------------------------------------------------- */
 
 
   /**
    * Read a list of user PHIDs from a request in a flexible way. This method
    * supports either of these forms:
    *
    *   users[]=alincoln&users[]=htaft
    *   users=alincoln,htaft
    *
    * Additionally, users can be specified either by PHID or by name.
    *
    * The main goal of this flexibility is to allow external programs to generate
    * links to pages (like "alincoln's open revisions") without needing to make
    * API calls.
    *
    * @param AphrontRequest  Request to read user PHIDs from.
    * @param string          Key to read in the request.
    * @param list<const>     Other permitted PHID types.
    * @return list<phid>     List of user PHIDs and selector functions.
    * @task read
    */
   protected function readUsersFromRequest(
     AphrontRequest $request,
     $key,
     array $allow_types = array()) {
 
     $list = $this->readListFromRequest($request, $key);
 
     $phids = array();
     $names = array();
     $allow_types = array_fuse($allow_types);
     $user_type = PhabricatorPeopleUserPHIDType::TYPECONST;
     foreach ($list as $item) {
       $type = phid_get_type($item);
       if ($type == $user_type) {
         $phids[] = $item;
       } else if (isset($allow_types[$type])) {
         $phids[] = $item;
       } else {
         if (PhabricatorTypeaheadDatasource::isFunctionToken($item)) {
           // If this is a function, pass it through unchanged; we'll evaluate
           // it later.
           $phids[] = $item;
         } else {
           $names[] = $item;
         }
       }
     }
 
     if ($names) {
       $users = id(new PhabricatorPeopleQuery())
         ->setViewer($this->requireViewer())
         ->withUsernames($names)
         ->execute();
       foreach ($users as $user) {
         $phids[] = $user->getPHID();
       }
       $phids = array_unique($phids);
     }
 
     return $phids;
   }
 
 
   /**
    * Read a list of project PHIDs from a request in a flexible way.
    *
    * @param AphrontRequest  Request to read user PHIDs from.
    * @param string          Key to read in the request.
    * @return list<phid>     List of projet PHIDs and selector functions.
    * @task read
    */
   protected function readProjectsFromRequest(AphrontRequest $request, $key) {
     $list = $this->readListFromRequest($request, $key);
 
     $phids = array();
     $slugs = array();
     $project_type = PhabricatorProjectProjectPHIDType::TYPECONST;
     foreach ($list as $item) {
       $type = phid_get_type($item);
       if ($type == $project_type) {
         $phids[] = $item;
       } else {
         if (PhabricatorTypeaheadDatasource::isFunctionToken($item)) {
           // If this is a function, pass it through unchanged; we'll evaluate
           // it later.
           $phids[] = $item;
         } else {
           $slugs[] = $item;
         }
       }
     }
 
     if ($slugs) {
       $projects = id(new PhabricatorProjectQuery())
         ->setViewer($this->requireViewer())
         ->withSlugs($slugs)
         ->execute();
       foreach ($projects as $project) {
         $phids[] = $project->getPHID();
       }
       $phids = array_unique($phids);
     }
 
     return $phids;
   }
 
 
   /**
    * Read a list of subscribers from a request in a flexible way.
    *
    * @param AphrontRequest  Request to read PHIDs from.
    * @param string          Key to read in the request.
    * @return list<phid>     List of object PHIDs.
    * @task read
    */
   protected function readSubscribersFromRequest(
     AphrontRequest $request,
     $key) {
     return $this->readUsersFromRequest(
       $request,
       $key,
       array(
         PhabricatorProjectProjectPHIDType::TYPECONST,
       ));
   }
 
 
   /**
    * Read a list of generic PHIDs from a request in a flexible way. Like
    * @{method:readUsersFromRequest}, this method supports either array or
    * comma-delimited forms. Objects can be specified either by PHID or by
    * object name.
    *
    * @param AphrontRequest  Request to read PHIDs from.
    * @param string          Key to read in the request.
    * @param list<const>     Optional, list of permitted PHID types.
    * @return list<phid>     List of object PHIDs.
    *
    * @task read
    */
   protected function readPHIDsFromRequest(
     AphrontRequest $request,
     $key,
     array $allow_types = array()) {
 
     $list = $this->readListFromRequest($request, $key);
 
     $objects = id(new PhabricatorObjectQuery())
       ->setViewer($this->requireViewer())
       ->withNames($list)
       ->execute();
     $list = mpull($objects, 'getPHID');
 
     if (!$list) {
       return array();
     }
 
     // If only certain PHID types are allowed, filter out all the others.
     if ($allow_types) {
       $allow_types = array_fuse($allow_types);
       foreach ($list as $key => $phid) {
         if (empty($allow_types[phid_get_type($phid)])) {
           unset($list[$key]);
         }
       }
     }
 
     return $list;
   }
 
 
   /**
    * Read a list of items from the request, in either array format or string
    * format:
    *
    *   list[]=item1&list[]=item2
    *   list=item1,item2
    *
    * This provides flexibility when constructing URIs, especially from external
    * sources.
    *
    * @param AphrontRequest  Request to read strings from.
    * @param string          Key to read in the request.
    * @return list<string>   List of values.
    */
   protected function readListFromRequest(
     AphrontRequest $request,
     $key) {
     $list = $request->getArr($key, null);
     if ($list === null) {
       $list = $request->getStrList($key);
     }
 
     if (!$list) {
       return array();
     }
 
     return $list;
   }
 
   protected function readDateFromRequest(
     AphrontRequest $request,
     $key) {
 
     $value = AphrontFormDateControlValue::newFromRequest($request, $key);
 
     if ($value->isEmpty()) {
       return null;
     }
 
     return $value->getDictionary();
   }
 
   protected function readBoolFromRequest(
     AphrontRequest $request,
     $key) {
     if (!strlen($request->getStr($key))) {
       return null;
     }
     return $request->getBool($key);
   }
 
 
   protected function getBoolFromQuery(PhabricatorSavedQuery $query, $key) {
     $value = $query->getParameter($key);
     if ($value === null) {
       return $value;
     }
     return $value ? 'true' : 'false';
   }
 
 
 /* -(  Dates  )-------------------------------------------------------------- */
 
 
   /**
    * @task dates
    */
   protected function parseDateTime($date_time) {
     if (!strlen($date_time)) {
       return null;
     }
 
     return PhabricatorTime::parseLocalTime($date_time, $this->requireViewer());
   }
 
 
   /**
    * @task dates
    */
   protected function buildDateRange(
     AphrontFormView $form,
     PhabricatorSavedQuery $saved_query,
     $start_key,
     $start_name,
     $end_key,
     $end_name) {
 
     $start_str = $saved_query->getParameter($start_key);
     $start = null;
     if (strlen($start_str)) {
       $start = $this->parseDateTime($start_str);
       if (!$start) {
         $this->addError(
           pht(
             '"%s" date can not be parsed.',
             $start_name));
       }
     }
 
 
     $end_str = $saved_query->getParameter($end_key);
     $end = null;
     if (strlen($end_str)) {
       $end = $this->parseDateTime($end_str);
       if (!$end) {
         $this->addError(
           pht(
             '"%s" date can not be parsed.',
             $end_name));
       }
     }
 
     if ($start && $end && ($start >= $end)) {
       $this->addError(
         pht(
           '"%s" must be a date before "%s".',
           $start_name,
           $end_name));
     }
 
     $form
       ->appendChild(
         id(new PHUIFormFreeformDateControl())
           ->setName($start_key)
           ->setLabel($start_name)
           ->setValue($start_str))
       ->appendChild(
         id(new AphrontFormTextControl())
           ->setName($end_key)
           ->setLabel($end_name)
           ->setValue($end_str));
   }
 
 
 /* -(  Paging and Executing Queries  )--------------------------------------- */
 
 
   public function getPageSize(PhabricatorSavedQuery $saved) {
     $limit = (int)$saved->getParameter('limit');
 
     if ($limit > 0) {
       return $limit;
     }
 
     return 100;
   }
 
 
   public function shouldUseOffsetPaging() {
     return false;
   }
 
 
   public function newPagerForSavedQuery(PhabricatorSavedQuery $saved) {
     if ($this->shouldUseOffsetPaging()) {
       $pager = new PHUIPagerView();
     } else {
       $pager = new AphrontCursorPagerView();
     }
 
     $page_size = $this->getPageSize($saved);
     if (is_finite($page_size)) {
       $pager->setPageSize($page_size);
     } else {
       // Consider an INF pagesize to mean a large finite pagesize.
 
       // TODO: It would be nice to handle this more gracefully, but math
       // with INF seems to vary across PHP versions, systems, and runtimes.
       $pager->setPageSize(0xFFFF);
     }
 
     return $pager;
   }
 
 
   public function executeQuery(
     PhabricatorPolicyAwareQuery $query,
     AphrontView $pager) {
 
     $query->setViewer($this->requireViewer());
 
     if ($this->shouldUseOffsetPaging()) {
       $objects = $query->executeWithOffsetPager($pager);
     } else {
       $objects = $query->executeWithCursorPager($pager);
     }
 
     return $objects;
   }
 
 
 /* -(  Rendering  )---------------------------------------------------------- */
 
 
   public function setRequest(AphrontRequest $request) {
     $this->request = $request;
     return $this;
   }
 
   public function getRequest() {
     return $this->request;
   }
 
   public function renderResults(
     array $objects,
     PhabricatorSavedQuery $query) {
 
     $phids = $this->getRequiredHandlePHIDsForResultList($objects, $query);
 
     if ($phids) {
       $handles = id(new PhabricatorHandleQuery())
         ->setViewer($this->requireViewer())
         ->witHPHIDs($phids)
         ->execute();
     } else {
       $handles = array();
     }
 
     return $this->renderResultList($objects, $query, $handles);
   }
 
   protected function getRequiredHandlePHIDsForResultList(
     array $objects,
     PhabricatorSavedQuery $query) {
     return array();
   }
 
   protected function renderResultList(
     array $objects,
     PhabricatorSavedQuery $query,
     array $handles) {
     throw new Exception(pht('Not supported here yet!'));
   }
 
 
 /* -(  Application Search  )------------------------------------------------- */
 
 
   /**
    * Retrieve an object to use to define custom fields for this search.
    *
    * To integrate with custom fields, subclasses should override this method
    * and return an instance of the application object which implements
    * @{interface:PhabricatorCustomFieldInterface}.
    *
    * @return PhabricatorCustomFieldInterface|null Object with custom fields.
    * @task appsearch
    */
   public function getCustomFieldObject() {
     $object = $this->newResultObject();
     if ($object instanceof PhabricatorCustomFieldInterface) {
       return $object;
     }
     return null;
   }
 
 
   /**
    * Get the custom fields for this search.
    *
    * @return PhabricatorCustomFieldList|null Custom fields, if this search
    *   supports custom fields.
    * @task appsearch
    */
   public function getCustomFieldList() {
     if ($this->customFields === false) {
       $object = $this->getCustomFieldObject();
       if ($object) {
         $fields = PhabricatorCustomField::getObjectFields(
           $object,
           PhabricatorCustomField::ROLE_APPLICATIONSEARCH);
         $fields->setViewer($this->requireViewer());
       } else {
         $fields = null;
       }
       $this->customFields = $fields;
     }
     return $this->customFields;
   }
 
 
   /**
    * Applies data from a saved query to an executable query.
    *
    * @param PhabricatorCursorPagedPolicyAwareQuery Query to constrain.
    * @param PhabricatorSavedQuery Saved query to read.
    * @return void
    */
   protected function applyCustomFieldsToQuery(
     PhabricatorCursorPagedPolicyAwareQuery $query,
     PhabricatorSavedQuery $saved) {
 
     $list = $this->getCustomFieldList();
     if (!$list) {
       return;
     }
 
     foreach ($list->getFields() as $field) {
       $value = $field->applyApplicationSearchConstraintToQuery(
         $this,
         $query,
         $saved->getParameter('custom:'.$field->getFieldIndex()));
     }
   }
 
   private function buildCustomFieldSearchFields() {
     $list = $this->getCustomFieldList();
     if (!$list) {
       return array();
     }
 
     $fields = array();
     foreach ($list->getFields() as $field) {
       $fields[] = id(new PhabricatorSearchCustomFieldProxyField())
         ->setSearchEngine($this)
         ->setCustomField($field);
     }
 
     return $fields;
   }
 
 }
diff --git a/src/applications/search/engine/PhabricatorJumpNavHandler.php b/src/applications/search/engine/PhabricatorJumpNavHandler.php
index 7a340a7e8..4a03ec9a8 100644
--- a/src/applications/search/engine/PhabricatorJumpNavHandler.php
+++ b/src/applications/search/engine/PhabricatorJumpNavHandler.php
@@ -1,122 +1,122 @@
 <?php
 
-final class PhabricatorJumpNavHandler {
+final class PhabricatorJumpNavHandler extends Phobject {
 
   public static function getJumpResponse(PhabricatorUser $viewer, $jump) {
     $jump = trim($jump);
 
     $patterns = array(
       '/^a$/i' => 'uri:/audit/',
       '/^f$/i' => 'uri:/feed/',
       '/^d$/i' => 'uri:/differential/',
       '/^r$/i' => 'uri:/diffusion/',
       '/^t$/i' => 'uri:/maniphest/',
       '/^p$/i' => 'uri:/project/',
       '/^u$/i' => 'uri:/people/',
       '/^p\s+(.+)$/i' => 'project',
       '/^u\s+(\S+)$/i' => 'user',
       '/^task:\s*(.+)/i' => 'create-task',
       '/^(?:s)\s+(\S+)/i' => 'find-symbol',
       '/^r\s+(.+)$/i' => 'find-repository',
     );
 
     foreach ($patterns as $pattern => $effect) {
       $matches = null;
       if (preg_match($pattern, $jump, $matches)) {
         if (!strncmp($effect, 'uri:', 4)) {
           return id(new AphrontRedirectResponse())
             ->setURI(substr($effect, 4));
         } else {
           switch ($effect) {
             case 'user':
               return id(new AphrontRedirectResponse())
                 ->setURI('/p/'.$matches[1].'/');
             case 'project':
               $project = self::findCloselyNamedProject($matches[1]);
               if ($project) {
                 return id(new AphrontRedirectResponse())
                   ->setURI('/project/view/'.$project->getID().'/');
               } else {
                   $jump = $matches[1];
               }
               break;
             case 'find-symbol':
               $context = '';
               $symbol = $matches[1];
               $parts = array();
               if (preg_match('/(.*)(?:\\.|::|->)(.*)/', $symbol, $parts)) {
                 $context = '&context='.phutil_escape_uri($parts[1]);
                 $symbol = $parts[2];
               }
               return id(new AphrontRedirectResponse())
                 ->setURI("/diffusion/symbol/$symbol/?jump=true$context");
             case 'find-repository':
               $name = $matches[1];
               $repositories = id(new PhabricatorRepositoryQuery())
                 ->setViewer($viewer)
                 ->withNameContains($name)
                 ->execute();
               if (count($repositories) == 1) {
                 // Just one match, jump to repository.
                 $uri = '/diffusion/'.head($repositories)->getCallsign().'/';
               } else {
                 // More than one match, jump to search.
                 $uri = urisprintf('/diffusion/?order=name&name=%s', $name);
               }
               return id(new AphrontRedirectResponse())->setURI($uri);
             case 'create-task':
               return id(new AphrontRedirectResponse())
                 ->setURI('/maniphest/task/create/?title='
                   .phutil_escape_uri($matches[1]));
             default:
               throw new Exception(pht("Unknown jump effect '%s'!", $effect));
           }
         }
       }
     }
 
     // If none of the patterns matched, look for an object by name.
     $objects = id(new PhabricatorObjectQuery())
       ->setViewer($viewer)
       ->withNames(array($jump))
       ->execute();
 
     if (count($objects) == 1) {
       $handle = id(new PhabricatorHandleQuery())
         ->setViewer($viewer)
         ->withPHIDs(mpull($objects, 'getPHID'))
         ->executeOne();
 
       return id(new AphrontRedirectResponse())->setURI($handle->getURI());
     }
 
     return null;
   }
 
   private static function findCloselyNamedProject($name) {
     $project = id(new PhabricatorProject())->loadOneWhere(
       'name = %s',
       $name);
     if ($project) {
       return $project;
     } else { // no exact match, try a fuzzy match
       $projects = id(new PhabricatorProject())->loadAllWhere(
         'name LIKE %~',
         $name);
       if ($projects) {
         $min_name_length = PHP_INT_MAX;
         $best_project = null;
         foreach ($projects as $project) {
           $name_length = strlen($project->getName());
           if ($name_length <= $min_name_length) {
             $min_name_length = $name_length;
             $best_project = $project;
           }
         }
         return $best_project;
       } else {
         return null;
       }
     }
   }
 }
diff --git a/src/applications/search/engine/PhabricatorSearchEngine.php b/src/applications/search/engine/PhabricatorSearchEngine.php
index 0ba8e321d..431d44792 100644
--- a/src/applications/search/engine/PhabricatorSearchEngine.php
+++ b/src/applications/search/engine/PhabricatorSearchEngine.php
@@ -1,162 +1,162 @@
 <?php
 
 /**
  * Base class for Phabricator search engine providers. Each engine must offer
  * three capabilities: indexing, searching, and reconstruction (this can be
  * stubbed out if an engine can't reasonably do it, it is used for debugging).
  */
-abstract class PhabricatorSearchEngine {
+abstract class PhabricatorSearchEngine extends Phobject {
 
 /* -(  Engine Metadata  )---------------------------------------------------- */
 
   /**
    * Return a unique, nonempty string which identifies this storage engine.
    *
    * @return string Unique string for this engine, max length 32.
    * @task meta
    */
   abstract public function getEngineIdentifier();
 
   /**
    * Prioritize this engine relative to other engines.
    *
    * Engines with a smaller priority number get an opportunity to write files
    * first. Generally, lower-latency filestores should have lower priority
    * numbers, and higher-latency filestores should have higher priority
    * numbers. Setting priority to approximately the number of milliseconds of
    * read latency will generally produce reasonable results.
    *
    * In conjunction with filesize limits, the goal is to store small files like
    * profile images, thumbnails, and text snippets in lower-latency engines,
    * and store large files in higher-capacity engines.
    *
    * @return float Engine priority.
    * @task meta
    */
   abstract public function getEnginePriority();
 
   /**
    * Return `true` if the engine is currently writable.
    *
    * Engines that are disabled or missing configuration should return `false`
    * to prevent new writes. If writes were made with this engine in the past,
    * the application may still try to perform reads.
    *
    * @return bool True if this engine can support new writes.
    * @task meta
    */
   abstract public function isEnabled();
 
 
 /* -(  Managing Documents  )------------------------------------------------- */
 
   /**
    * Update the index for an abstract document.
    *
    * @param PhabricatorSearchAbstractDocument Document to update.
    * @return void
    */
   abstract public function reindexAbstractDocument(
     PhabricatorSearchAbstractDocument $document);
 
   /**
    * Reconstruct the document for a given PHID. This is used for debugging
    * and does not need to be perfect if it is unreasonable to implement it.
    *
    * @param  phid Document PHID to reconstruct.
    * @return PhabricatorSearchAbstractDocument Abstract document.
    */
   abstract public function reconstructDocument($phid);
 
   /**
    * Execute a search query.
    *
    * @param PhabricatorSavedQuery A query to execute.
    * @return list A list of matching PHIDs.
    */
   abstract public function executeSearch(PhabricatorSavedQuery $query);
 
   /**
    * Does the search index exist?
    *
    * @return bool
    */
   abstract public function indexExists();
 
   /**
    * Is the index in a usable state?
    *
    * @return bool
    */
   public function indexIsSane() {
     return $this->indexExists();
   }
 
   /**
    * Do any sort of setup for the search index.
    *
    * @return void
    */
   public function initIndex() {}
 
 
 /* -(  Loading Storage Engines  )-------------------------------------------- */
 
   /**
    * @task load
    */
   public static function loadAllEngines() {
     static $engines;
 
     if ($engines === null) {
       $objects = id(new PhutilSymbolLoader())
         ->setAncestorClass(__CLASS__)
         ->loadObjects();
 
       $map = array();
       foreach ($objects as $engine) {
         $key = $engine->getEngineIdentifier();
         if (empty($map[$key])) {
           $map[$key] = $engine;
         } else {
           throw new Exception(
             pht(
               'Search engines "%s" and "%s" have the same engine identifier '.
               '"%s". Each storage engine must have a unique identifier.',
               get_class($engine),
               get_class($map[$key]),
               $key));
         }
       }
 
       $map = msort($map, 'getEnginePriority');
 
       $engines = $map;
     }
 
     return $engines;
   }
 
   /**
    * @task load
    */
   public static function loadActiveEngines() {
     $engines = self::loadAllEngines();
 
     $active = array();
     foreach ($engines as $key => $engine) {
       if (!$engine->isEnabled()) {
         continue;
       }
 
       $active[$key] = $engine;
     }
 
     return $active;
   }
 
   public static function loadEngine() {
     return head(self::loadActiveEngines());
   }
 
 }
diff --git a/src/applications/search/engine/__tests__/PhabricatorApplicationSearchEngineTestCase.php b/src/applications/search/engine/__tests__/PhabricatorApplicationSearchEngineTestCase.php
new file mode 100644
index 000000000..488f4692a
--- /dev/null
+++ b/src/applications/search/engine/__tests__/PhabricatorApplicationSearchEngineTestCase.php
@@ -0,0 +1,11 @@
+<?php
+
+final class PhabricatorApplicationSearchEngineTestCase
+  extends PhabricatorTestCase {
+
+  public function testGetAllEngines() {
+    PhabricatorApplicationSearchEngine::getAllEngines();
+    $this->assertTrue(true);
+  }
+
+}
diff --git a/src/applications/search/engine/__tests__/PhabricatorSearchEngineTestCase.php b/src/applications/search/engine/__tests__/PhabricatorSearchEngineTestCase.php
new file mode 100644
index 000000000..bf997288b
--- /dev/null
+++ b/src/applications/search/engine/__tests__/PhabricatorSearchEngineTestCase.php
@@ -0,0 +1,10 @@
+<?php
+
+final class PhabricatorSearchEngineTestCase extends PhabricatorTestCase {
+
+  public function testLoadAllEngines() {
+    PhabricatorSearchEngine::loadAllEngines();
+    $this->assertTrue(true);
+  }
+
+}
diff --git a/src/applications/search/index/PhabricatorSearchAbstractDocument.php b/src/applications/search/index/PhabricatorSearchAbstractDocument.php
index 09923c7dc..0e57be848 100644
--- a/src/applications/search/index/PhabricatorSearchAbstractDocument.php
+++ b/src/applications/search/index/PhabricatorSearchAbstractDocument.php
@@ -1,76 +1,76 @@
 <?php
 
-final class PhabricatorSearchAbstractDocument {
+final class PhabricatorSearchAbstractDocument extends Phobject {
 
   private $phid;
   private $documentType;
   private $documentTitle;
   private $documentCreated;
   private $documentModified;
   private $fields = array();
   private $relationships = array();
 
   public function setPHID($phid) {
     $this->phid = $phid;
     return $this;
   }
 
   public function setDocumentType($document_type) {
     $this->documentType = $document_type;
     return $this;
   }
 
   public function setDocumentTitle($title) {
     $this->documentTitle = $title;
     $this->addField(PhabricatorSearchDocumentFieldType::FIELD_TITLE, $title);
     return $this;
   }
 
   public function addField($field, $corpus, $aux_phid = null) {
     $this->fields[] = array($field, $corpus, $aux_phid);
     return $this;
   }
 
   public function addRelationship($type, $related_phid, $rtype, $time) {
     $this->relationships[] = array($type, $related_phid, $rtype, $time);
     return $this;
   }
 
   public function setDocumentCreated($date) {
     $this->documentCreated = $date;
     return $this;
   }
 
   public function setDocumentModified($date) {
     $this->documentModified = $date;
     return $this;
   }
 
   public function getPHID() {
     return $this->phid;
   }
 
   public function getDocumentType() {
     return $this->documentType;
   }
 
   public function getDocumentTitle() {
     return $this->documentTitle;
   }
 
   public function getDocumentCreated() {
     return $this->documentCreated;
   }
 
   public function getDocumentModified() {
     return $this->documentModified;
   }
 
   public function getFieldData() {
     return $this->fields;
   }
 
   public function getRelationshipData() {
     return $this->relationships;
   }
 }
diff --git a/src/applications/search/index/PhabricatorSearchIndexer.php b/src/applications/search/index/PhabricatorSearchIndexer.php
index 0ba69373a..137a030e2 100644
--- a/src/applications/search/index/PhabricatorSearchIndexer.php
+++ b/src/applications/search/index/PhabricatorSearchIndexer.php
@@ -1,32 +1,32 @@
 <?php
 
-final class PhabricatorSearchIndexer {
+final class PhabricatorSearchIndexer extends Phobject {
 
   public function queueDocumentForIndexing($phid, $context = null) {
     PhabricatorWorker::scheduleTask(
       'PhabricatorSearchWorker',
       array(
         'documentPHID' => $phid,
         'context' => $context,
       ),
       array(
         'priority' => PhabricatorWorker::PRIORITY_IMPORT,
       ));
   }
 
   public function indexDocumentByPHID($phid, $context) {
     $indexers = id(new PhutilSymbolLoader())
       ->setAncestorClass('PhabricatorSearchDocumentIndexer')
       ->loadObjects();
 
     foreach ($indexers as $indexer) {
       if ($indexer->shouldIndexDocumentByPHID($phid)) {
         $indexer->indexDocumentByPHID($phid, $context);
         break;
       }
     }
 
     return $this;
   }
 
 }
diff --git a/src/applications/settings/panel/PhabricatorDateTimeSettingsPanel.php b/src/applications/settings/panel/PhabricatorDateTimeSettingsPanel.php
index 5fbf825d2..45f9f3fa0 100644
--- a/src/applications/settings/panel/PhabricatorDateTimeSettingsPanel.php
+++ b/src/applications/settings/panel/PhabricatorDateTimeSettingsPanel.php
@@ -1,115 +1,106 @@
 <?php
 
 final class PhabricatorDateTimeSettingsPanel extends PhabricatorSettingsPanel {
 
   public function getPanelKey() {
     return 'datetime';
   }
 
   public function getPanelName() {
     return pht('Date and Time');
   }
 
   public function getPanelGroup() {
     return pht('Account Information');
   }
 
   public function processRequest(AphrontRequest $request) {
     $user = $request->getUser();
     $username = $user->getUsername();
 
     $pref_time = PhabricatorUserPreferences::PREFERENCE_TIME_FORMAT;
     $pref_week_start = PhabricatorUserPreferences::PREFERENCE_WEEK_START_DAY;
     $preferences = $user->loadPreferences();
 
     $errors = array();
     if ($request->isFormPost()) {
       $new_timezone = $request->getStr('timezone');
       if (in_array($new_timezone, DateTimeZone::listIdentifiers(), true)) {
         $user->setTimezoneIdentifier($new_timezone);
       } else {
         $errors[] = pht('The selected timezone is not a valid timezone.');
       }
 
       $preferences->setPreference(
         $pref_time,
         $request->getStr($pref_time));
       $preferences->setPreference(
         $pref_week_start,
         $request->getStr($pref_week_start));
 
       if (!$errors) {
         $preferences->save();
         $user->save();
         return id(new AphrontRedirectResponse())
           ->setURI($this->getPanelURI('?saved=true'));
       }
     }
 
     $timezone_ids = DateTimeZone::listIdentifiers();
     $timezone_id_map = array_fuse($timezone_ids);
 
     $form = new AphrontFormView();
     $form
       ->setUser($user)
       ->appendChild(
         id(new AphrontFormSelectControl())
           ->setLabel(pht('Timezone'))
           ->setName('timezone')
           ->setOptions($timezone_id_map)
           ->setValue($user->getTimezoneIdentifier()))
-      ->appendRemarkupInstructions(
-        pht(
-          "**Custom Date and Time Formats**\n\n".
-          "You can specify custom formats which will be used when ".
-          "rendering dates and times of day. Examples:\n\n".
-          "| Format  | Example  | Notes |\n".
-          "| ------  | -------- | ----- |\n".
-          "| `g:i A` | 2:34 PM  | Default 12-hour time. |\n".
-          "| `G.i a` | 02.34 pm | Alternate 12-hour time. |\n".
-          "| `H:i`   | 14:34    | 24-hour time. |\n".
-          "\n\n".
-          "You can find a [[%s | full reference in the PHP manual]].",
-          'http://www.php.net/manual/en/function.date.php'))
       ->appendChild(
-        id(new AphrontFormTextControl())
+        id(new AphrontFormSelectControl())
           ->setLabel(pht('Time-of-Day Format'))
           ->setName($pref_time)
+          ->setOptions(array(
+              'g:i A' => pht('12-hour (2:34 PM)'),
+              'H:i' => pht('24-hour (14:34)'),
+            ))
           ->setCaption(
             pht('Format used when rendering a time of day.'))
           ->setValue($preferences->getPreference($pref_time)))
       ->appendChild(
         id(new AphrontFormSelectControl())
           ->setLabel(pht('Week Starts On'))
           ->setOptions($this->getWeekDays())
           ->setName($pref_week_start)
           ->setCaption(
             pht('Calendar weeks will start with this day.'))
           ->setValue($preferences->getPreference($pref_week_start, 0)))
       ->appendChild(
         id(new AphrontFormSubmitControl())
           ->setValue(pht('Save Account Settings')));
 
     $form_box = id(new PHUIObjectBoxView())
       ->setHeaderText(pht('Date and Time Settings'))
       ->setFormSaved($request->getStr('saved'))
       ->setFormErrors($errors)
       ->setForm($form);
 
     return array(
       $form_box,
     );
   }
 
   private function getWeekDays() {
     return array(
       pht('Sunday'),
       pht('Monday'),
       pht('Tuesday'),
       pht('Wednesday'),
       pht('Thursday'),
       pht('Friday'),
       pht('Saturday'),
     );
   }
 }
diff --git a/src/applications/settings/panel/PhabricatorSettingsPanel.php b/src/applications/settings/panel/PhabricatorSettingsPanel.php
index f1dacce3a..341b7a4f6 100644
--- a/src/applications/settings/panel/PhabricatorSettingsPanel.php
+++ b/src/applications/settings/panel/PhabricatorSettingsPanel.php
@@ -1,190 +1,190 @@
 <?php
 
 /**
  * Defines a settings panel. Settings panels appear in the Settings application,
  * and behave like lightweight controllers -- generally, they render some sort
  * of form with options in it, and then update preferences when the user
  * submits the form. By extending this class, you can add new settings
  * panels.
  *
  * NOTE: This stuff is new and might not be completely stable.
  *
  * @task config   Panel Configuration
  * @task panel    Panel Implementation
  * @task internal Internals
  */
-abstract class PhabricatorSettingsPanel {
+abstract class PhabricatorSettingsPanel extends Phobject {
 
   private $user;
   private $viewer;
   private $overrideURI;
 
 
   public function setUser(PhabricatorUser $user) {
     $this->user = $user;
     return $this;
   }
 
   public function getUser() {
     return $this->user;
   }
 
   public function setViewer(PhabricatorUser $viewer) {
     $this->viewer = $viewer;
     return $this;
   }
 
   public function getViewer() {
     return $this->viewer;
   }
 
   public function setOverrideURI($override_uri) {
     $this->overrideURI = $override_uri;
     return $this;
   }
 
 
 /* -(  Panel Configuration  )------------------------------------------------ */
 
 
   /**
    * Return a unique string used in the URI to identify this panel, like
    * "example".
    *
    * @return string Unique panel identifier (used in URIs).
    * @task config
    */
   abstract public function getPanelKey();
 
 
   /**
    * Return a human-readable description of the panel's contents, like
    * "Example Settings".
    *
    * @return string Human-readable panel name.
    * @task config
    */
   abstract public function getPanelName();
 
 
   /**
    * Return a human-readable group name for this panel. For instance, if you
    * had several related panels like "Volume Settings" and
    * "Microphone Settings", you might put them in a group called "Audio".
    *
    * When displayed, panels are grouped with other panels that have the same
    * group name.
    *
    * @return string Human-readable panel group name.
    * @task config
    */
   abstract public function getPanelGroup();
 
 
   /**
    * Return false to prevent this panel from being displayed or used. You can
    * do, e.g., configuration checks here, to determine if the feature your
    * panel controls is unavailble in this install. By default, all panels are
    * enabled.
    *
    * @return bool True if the panel should be shown.
    * @task config
    */
   public function isEnabled() {
     return true;
   }
 
 
   /**
    * You can use this callback to generate multiple similar panels which all
    * share the same implementation. For example, OAuth providers each have a
    * separate panel, but the implementation for each panel is the same.
    *
    * To generate multiple panels, build them here and return a list. By default,
    * the current panel (`$this`) is returned alone. For most panels, this
    * is the right implementation.
    *
    * @return list<PhabricatorSettingsPanel> Zero or more panels.
    * @task config
    */
   public function buildPanels() {
     return array($this);
   }
 
 
   /**
    * Return true if this panel is available to administrators while editing
    * system agent accounts.
    *
    * @return bool True to enable edit by administrators.
    * @task config
    */
   public function isEditableByAdministrators() {
     return false;
   }
 
 
 /* -(  Panel Implementation  )----------------------------------------------- */
 
 
   /**
    * Process a user request for this settings panel. Implement this method like
    * a lightweight controller. If you return an @{class:AphrontResponse}, the
    * response will be used in whole. If you return anything else, it will be
    * treated as a view and composed into a normal settings page.
    *
    * Generally, render your settings panel by returning a form, then return
    * a redirect when the user saves settings.
    *
    * @param   AphrontRequest  Incoming request.
    * @return  wild            Response to request, either as an
    *                          @{class:AphrontResponse} or something which can
    *                          be composed into a @{class:AphrontView}.
    * @task panel
    */
   abstract public function processRequest(AphrontRequest $request);
 
 
   /**
    * Get the URI for this panel.
    *
    * @param string? Optional path to append.
    * @return string Relative URI for the panel.
    * @task panel
    */
   final public function getPanelURI($path = '') {
     $path = ltrim($path, '/');
 
     if ($this->overrideURI) {
       return rtrim($this->overrideURI, '/').'/'.$path;
     }
 
     $key = $this->getPanelKey();
     $key = phutil_escape_uri($key);
 
     if ($this->getUser()->getPHID() != $this->getViewer()->getPHID()) {
       $user_id = $this->getUser()->getID();
       return "/settings/{$user_id}/panel/{$key}/{$path}";
     } else {
       return "/settings/panel/{$key}/{$path}";
     }
   }
 
 
 /* -(  Internals  )---------------------------------------------------------- */
 
 
   /**
    * Generates a key to sort the list of panels.
    *
    * @return string Sortable key.
    * @task internal
    */
   final public function getPanelSortKey() {
     return sprintf(
       '%s'.chr(255).'%s',
       $this->getPanelGroup(),
       $this->getPanelName());
   }
 
 }
diff --git a/src/applications/slowvote/application/PhabricatorSlowvoteApplication.php b/src/applications/slowvote/application/PhabricatorSlowvoteApplication.php
index fa3acfd77..40791659b 100644
--- a/src/applications/slowvote/application/PhabricatorSlowvoteApplication.php
+++ b/src/applications/slowvote/application/PhabricatorSlowvoteApplication.php
@@ -1,71 +1,72 @@
 <?php
 
 final class PhabricatorSlowvoteApplication extends PhabricatorApplication {
 
   public function getBaseURI() {
     return '/vote/';
   }
 
   public function getFontIcon() {
     return 'fa-bar-chart';
   }
 
   public function getName() {
     return pht('Slowvote');
   }
 
   public function getShortDescription() {
     return pht('Conduct Polls');
   }
 
   public function getTitleGlyph() {
     return "\xE2\x9C\x94";
   }
 
   public function getHelpDocumentationArticles(PhabricatorUser $viewer) {
     return array(
       array(
         'name' => pht('Slowvote User Guide'),
         'href' => PhabricatorEnv::getDoclink('Slowvote User Guide'),
       ),
     );
   }
 
   public function getFlavorText() {
     return pht('Design by committee.');
   }
 
   public function getApplicationGroup() {
     return self::GROUP_UTILITIES;
   }
 
   public function getRemarkupRules() {
     return array(
       new SlowvoteRemarkupRule(),
     );
   }
 
   public function getRoutes() {
     return array(
       '/V(?P<id>[1-9]\d*)' => 'PhabricatorSlowvotePollController',
       '/vote/' => array(
         '(?:query/(?P<queryKey>[^/]+)/)?'
           => 'PhabricatorSlowvoteListController',
         'create/' => 'PhabricatorSlowvoteEditController',
         'edit/(?P<id>[1-9]\d*)/' => 'PhabricatorSlowvoteEditController',
         '(?P<id>[1-9]\d*)/' => 'PhabricatorSlowvoteVoteController',
         'comment/(?P<id>[1-9]\d*)/' => 'PhabricatorSlowvoteCommentController',
         'close/(?P<id>[1-9]\d*)/' => 'PhabricatorSlowvoteCloseController',
       ),
     );
   }
 
   protected function getCustomCapabilities() {
     return array(
       PhabricatorSlowvoteDefaultViewCapability::CAPABILITY => array(
         'caption' => pht('Default view policy for new polls.'),
+        'template' => PhabricatorSlowvotePollPHIDType::TYPECONST,
       ),
     );
   }
 
 }
diff --git a/src/applications/spaces/application/PhabricatorSpacesApplication.php b/src/applications/spaces/application/PhabricatorSpacesApplication.php
index 380f80569..7942caf91 100644
--- a/src/applications/spaces/application/PhabricatorSpacesApplication.php
+++ b/src/applications/spaces/application/PhabricatorSpacesApplication.php
@@ -1,73 +1,86 @@
 <?php
 
 final class PhabricatorSpacesApplication extends PhabricatorApplication {
 
   public function getBaseURI() {
     return '/spaces/';
   }
 
   public function getName() {
     return pht('Spaces');
   }
 
   public function getShortDescription() {
     return pht('Policy Namespaces');
   }
 
   public function getFontIcon() {
     return 'fa-th-large';
   }
 
   public function getTitleGlyph() {
     return "\xE2\x97\x8B";
   }
 
   public function getFlavorText() {
     return pht('Control access to groups of objects.');
   }
 
   public function getApplicationGroup() {
     return self::GROUP_UTILITIES;
   }
 
   public function canUninstall() {
     return true;
   }
 
   public function isPrototype() {
     return true;
   }
 
+  public function getHelpDocumentationArticles(PhabricatorUser $viewer) {
+    return array(
+      array(
+        'name' => pht('Spaces User Guide'),
+        'href' => PhabricatorEnv::getDoclink('Spaces User Guide'),
+      ),
+    );
+  }
+
   public function getRemarkupRules() {
     return array(
       new PhabricatorSpacesRemarkupRule(),
     );
   }
 
   public function getRoutes() {
     return array(
       '/S(?P<id>[1-9]\d*)' => 'PhabricatorSpacesViewController',
       '/spaces/' => array(
         '(?:query/(?P<queryKey>[^/]+)/)?' => 'PhabricatorSpacesListController',
         'create/' => 'PhabricatorSpacesEditController',
         'edit/(?:(?P<id>\d+)/)?' => 'PhabricatorSpacesEditController',
+        '(?P<action>activate|archive)/(?P<id>\d+)/'
+          => 'PhabricatorSpacesArchiveController',
       ),
     );
   }
 
   protected function getCustomCapabilities() {
     return array(
       PhabricatorSpacesCapabilityCreateSpaces::CAPABILITY => array(
         'default' => PhabricatorPolicies::POLICY_ADMIN,
       ),
       PhabricatorSpacesCapabilityDefaultView::CAPABILITY => array(
         'caption' => pht('Default view policy for newly created spaces.'),
+        'template' => PhabricatorSpacesNamespacePHIDType::TYPECONST,
       ),
       PhabricatorSpacesCapabilityDefaultEdit::CAPABILITY => array(
         'caption' => pht('Default edit policy for newly created spaces.'),
         'default' => PhabricatorPolicies::POLICY_ADMIN,
+        'template' => PhabricatorSpacesNamespacePHIDType::TYPECONST,
       ),
     );
   }
 
 }
diff --git a/src/applications/spaces/controller/PhabricatorSpacesArchiveController.php b/src/applications/spaces/controller/PhabricatorSpacesArchiveController.php
new file mode 100644
index 000000000..82cf19e9e
--- /dev/null
+++ b/src/applications/spaces/controller/PhabricatorSpacesArchiveController.php
@@ -0,0 +1,76 @@
+<?php
+
+final class PhabricatorSpacesArchiveController
+  extends PhabricatorSpacesController {
+
+  public function handleRequest(AphrontRequest $request) {
+    $viewer = $request->getUser();
+
+    $space = id(new PhabricatorSpacesNamespaceQuery())
+      ->setViewer($viewer)
+      ->withIDs(array($request->getURIData('id')))
+      ->requireCapabilities(
+        array(
+          PhabricatorPolicyCapability::CAN_VIEW,
+          PhabricatorPolicyCapability::CAN_EDIT,
+        ))
+      ->executeOne();
+    if (!$space) {
+      return new Aphront404Response();
+    }
+
+    $is_archive = ($request->getURIData('action') == 'archive');
+    $cancel_uri = '/'.$space->getMonogram();
+
+    if ($request->isFormPost()) {
+      $type_archive = PhabricatorSpacesNamespaceTransaction::TYPE_ARCHIVE;
+
+      $xactions = array();
+      $xactions[] = id(new PhabricatorSpacesNamespaceTransaction())
+        ->setTransactionType($type_archive)
+        ->setNewValue($is_archive ? 1 : 0);
+
+      $editor = id(new PhabricatorSpacesNamespaceEditor())
+        ->setActor($viewer)
+        ->setContinueOnNoEffect(true)
+        ->setContinueOnMissingFields(true)
+        ->setContentSourceFromRequest($request);
+
+      $editor->applyTransactions($space, $xactions);
+
+      return id(new AphrontRedirectResponse())->setURI($cancel_uri);
+    }
+
+    $body = array();
+    if ($is_archive) {
+      $title = pht('Archive Space: %s', $space->getNamespaceName());
+      $body[] = pht(
+        'If you archive this Space, you will no longer be able to create '.
+        'new objects inside it.');
+      $body[] = pht(
+        'Existing objects in this Space will be hidden from query results '.
+        'by default.');
+      $button = pht('Archive Space');
+    } else {
+      $title = pht('Activate Space: %s', $space->getNamespaceName());
+      $body[] = pht(
+        'If you activate this space, you will be able to create objects '.
+        'inside it again.');
+      $body[] = pht(
+        'Existing objects will no longer be hidden from query results.');
+      $button = pht('Activate Space');
+    }
+
+
+    $dialog = $this->newDialog()
+      ->setTitle($title)
+      ->addCancelButton($cancel_uri)
+      ->addSubmitButton($button);
+
+    foreach ($body as $paragraph) {
+      $dialog->appendParagraph($paragraph);
+    }
+
+    return $dialog;
+  }
+}
diff --git a/src/applications/spaces/controller/PhabricatorSpacesEditController.php b/src/applications/spaces/controller/PhabricatorSpacesEditController.php
index b64a16ab9..4dfeeed7e 100644
--- a/src/applications/spaces/controller/PhabricatorSpacesEditController.php
+++ b/src/applications/spaces/controller/PhabricatorSpacesEditController.php
@@ -1,174 +1,186 @@
 <?php
 
 final class PhabricatorSpacesEditController
   extends PhabricatorSpacesController {
 
   public function handleRequest(AphrontRequest $request) {
     $viewer = $request->getUser();
 
     $make_default = false;
 
     $id = $request->getURIData('id');
     if ($id) {
       $space = id(new PhabricatorSpacesNamespaceQuery())
         ->setViewer($viewer)
         ->withIDs(array($id))
         ->requireCapabilities(
           array(
             PhabricatorPolicyCapability::CAN_VIEW,
             PhabricatorPolicyCapability::CAN_EDIT,
           ))
         ->executeOne();
       if (!$space) {
         return new Aphront404Response();
       }
 
       $is_new = false;
       $cancel_uri = '/'.$space->getMonogram();
 
       $header_text = pht('Edit %s', $space->getNamespaceName());
       $title = pht('Edit Space');
       $button_text = pht('Save Changes');
     } else {
       $this->requireApplicationCapability(
         PhabricatorSpacesCapabilityCreateSpaces::CAPABILITY);
 
       $space = PhabricatorSpacesNamespace::initializeNewNamespace($viewer);
 
       $is_new = true;
       $cancel_uri = $this->getApplicationURI();
 
       $header_text = pht('Create Space');
       $title = pht('Create Space');
       $button_text = pht('Create Space');
 
       $default = id(new PhabricatorSpacesNamespaceQuery())
         ->setViewer(PhabricatorUser::getOmnipotentUser())
         ->withIsDefaultNamespace(true)
         ->execute();
       if (!$default) {
         $make_default = true;
       }
     }
 
     $validation_exception = null;
     $e_name = true;
     $v_name = $space->getNamespaceName();
+    $v_desc = $space->getDescription();
     $v_view = $space->getViewPolicy();
     $v_edit = $space->getEditPolicy();
 
     if ($request->isFormPost()) {
       $xactions = array();
       $e_name = null;
 
       $v_name = $request->getStr('name');
+      $v_desc = $request->getStr('description');
       $v_view = $request->getStr('viewPolicy');
       $v_edit = $request->getStr('editPolicy');
 
       $type_name = PhabricatorSpacesNamespaceTransaction::TYPE_NAME;
+      $type_desc = PhabricatorSpacesNamespaceTransaction::TYPE_DESCRIPTION;
       $type_default = PhabricatorSpacesNamespaceTransaction::TYPE_DEFAULT;
       $type_view = PhabricatorTransactions::TYPE_VIEW_POLICY;
       $type_edit = PhabricatorTransactions::TYPE_EDIT_POLICY;
 
       $xactions[] = id(new PhabricatorSpacesNamespaceTransaction())
         ->setTransactionType($type_name)
         ->setNewValue($v_name);
 
+      $xactions[] = id(new PhabricatorSpacesNamespaceTransaction())
+        ->setTransactionType($type_desc)
+        ->setNewValue($v_desc);
+
       $xactions[] = id(new PhabricatorSpacesNamespaceTransaction())
         ->setTransactionType($type_view)
         ->setNewValue($v_view);
 
       $xactions[] = id(new PhabricatorSpacesNamespaceTransaction())
         ->setTransactionType($type_edit)
         ->setNewValue($v_edit);
 
       if ($make_default) {
         $xactions[] = id(new PhabricatorSpacesNamespaceTransaction())
           ->setTransactionType($type_default)
           ->setNewValue(1);
       }
 
       $editor = id(new PhabricatorSpacesNamespaceEditor())
         ->setActor($viewer)
         ->setContinueOnNoEffect(true)
         ->setContentSourceFromRequest($request);
 
       try {
         $editor->applyTransactions($space, $xactions);
 
         return id(new AphrontRedirectResponse())
           ->setURI('/'.$space->getMonogram());
       } catch (PhabricatorApplicationTransactionValidationException $ex) {
         $validation_exception = $ex;
 
         $e_name = $ex->getShortMessage($type_name);
       }
     }
 
     $policies = id(new PhabricatorPolicyQuery())
       ->setViewer($viewer)
       ->setObject($space)
       ->execute();
 
     $form = id(new AphrontFormView())
       ->setUser($viewer);
 
     if ($make_default) {
       $form->appendRemarkupInstructions(
         pht(
           'NOTE: You are creating the **default space**. All existing '.
           'objects will be put into this space. You must create a default '.
           'space before you can create other spaces.'));
     }
 
     $form
       ->appendChild(
         id(new AphrontFormTextControl())
           ->setLabel(pht('Name'))
           ->setName('name')
           ->setValue($v_name)
           ->setError($e_name))
+      ->appendControl(
+        id(new PhabricatorRemarkupControl())
+          ->setLabel(pht('Description'))
+          ->setName('description')
+          ->setValue($v_desc))
       ->appendChild(
         id(new AphrontFormPolicyControl())
           ->setUser($viewer)
           ->setCapability(PhabricatorPolicyCapability::CAN_VIEW)
           ->setPolicyObject($space)
           ->setPolicies($policies)
           ->setValue($v_view)
           ->setName('viewPolicy'))
       ->appendChild(
         id(new AphrontFormPolicyControl())
           ->setUser($viewer)
           ->setCapability(PhabricatorPolicyCapability::CAN_EDIT)
           ->setPolicyObject($space)
           ->setPolicies($policies)
           ->setValue($v_edit)
           ->setName('editPolicy'))
       ->appendChild(
         id(new AphrontFormSubmitControl())
           ->setValue($button_text)
           ->addCancelButton($cancel_uri));
 
     $box = id(new PHUIObjectBoxView())
       ->setHeaderText($header_text)
       ->setValidationException($validation_exception)
       ->appendChild($form);
 
     $crumbs = $this->buildApplicationCrumbs();
     if (!$is_new) {
       $crumbs->addTextCrumb(
         $space->getMonogram(),
         $cancel_uri);
     }
     $crumbs->addTextCrumb($title);
 
     return $this->buildApplicationPage(
       array(
         $crumbs,
         $box,
       ),
       array(
         'title' => $title,
       ));
   }
 }
diff --git a/src/applications/spaces/controller/PhabricatorSpacesViewController.php b/src/applications/spaces/controller/PhabricatorSpacesViewController.php
index 911549234..511d85315 100644
--- a/src/applications/spaces/controller/PhabricatorSpacesViewController.php
+++ b/src/applications/spaces/controller/PhabricatorSpacesViewController.php
@@ -1,104 +1,144 @@
 <?php
 
 final class PhabricatorSpacesViewController
   extends PhabricatorSpacesController {
 
   public function shouldAllowPublic() {
     return true;
   }
 
   public function handleRequest(AphrontRequest $request) {
     $viewer = $this->getViewer();
 
     $space = id(new PhabricatorSpacesNamespaceQuery())
       ->setViewer($viewer)
       ->withIDs(array($request->getURIData('id')))
       ->executeOne();
     if (!$space) {
       return new Aphront404Response();
     }
 
     $action_list = $this->buildActionListView($space);
     $property_list = $this->buildPropertyListView($space);
     $property_list->setActionList($action_list);
 
     $xactions = id(new PhabricatorSpacesNamespaceTransactionQuery())
       ->setViewer($viewer)
       ->withObjectPHIDs(array($space->getPHID()))
       ->execute();
 
     $timeline = $this->buildTransactionTimeline(
       $space,
       new PhabricatorSpacesNamespaceTransactionQuery());
     $timeline->setShouldTerminate(true);
 
     $header = id(new PHUIHeaderView())
       ->setUser($viewer)
       ->setHeader($space->getNamespaceName())
       ->setPolicyObject($space);
 
+    if ($space->getIsArchived()) {
+      $header->setStatus('fa-ban', 'red', pht('Archived'));
+    } else {
+      $header->setStatus('fa-check', 'bluegrey', pht('Active'));
+    }
+
     $box = id(new PHUIObjectBoxView())
       ->setHeader($header)
       ->addPropertyList($property_list);
 
     $crumbs = $this->buildApplicationCrumbs();
     $crumbs->addTextCrumb($space->getMonogram());
 
     return $this->buildApplicationPage(
       array(
         $crumbs,
         $box,
         $timeline,
       ),
       array(
         'title' => array($space->getMonogram(), $space->getNamespaceName()),
       ));
   }
 
   private function buildPropertyListView(PhabricatorSpacesNamespace $space) {
     $viewer = $this->getRequest()->getUser();
 
     $list = id(new PHUIPropertyListView())
       ->setUser($viewer);
 
     $list->addProperty(
       pht('Default Space'),
       $space->getIsDefaultNamespace()
         ? pht('Yes')
         : pht('No'));
 
     $descriptions = PhabricatorPolicyQuery::renderPolicyDescriptions(
       $viewer,
       $space);
 
     $list->addProperty(
       pht('Editable By'),
       $descriptions[PhabricatorPolicyCapability::CAN_EDIT]);
 
+    $description = $space->getDescription();
+    if (strlen($description)) {
+      $description = PhabricatorMarkupEngine::renderOneObject(
+        id(new PhabricatorMarkupOneOff())->setContent($description),
+        'default',
+        $viewer);
+
+      $list->addSectionHeader(
+        pht('Description'),
+        PHUIPropertyListView::ICON_SUMMARY);
+
+      $list->addTextContent($description);
+    }
+
     return $list;
   }
 
   private function buildActionListView(PhabricatorSpacesNamespace $space) {
     $viewer = $this->getRequest()->getUser();
 
     $list = id(new PhabricatorActionListView())
       ->setUser($viewer)
       ->setObjectURI('/'.$space->getMonogram());
 
     $can_edit = PhabricatorPolicyFilter::hasCapability(
       $viewer,
       $space,
       PhabricatorPolicyCapability::CAN_EDIT);
 
     $list->addAction(
       id(new PhabricatorActionView())
         ->setName(pht('Edit Space'))
         ->setIcon('fa-pencil')
         ->setHref($this->getApplicationURI('edit/'.$space->getID().'/'))
         ->setWorkflow(!$can_edit)
         ->setDisabled(!$can_edit));
 
+    $id = $space->getID();
+
+    if ($space->getIsArchived()) {
+      $list->addAction(
+        id(new PhabricatorActionView())
+          ->setName(pht('Activate Space'))
+          ->setIcon('fa-check')
+          ->setHref($this->getApplicationURI("activate/{$id}/"))
+          ->setDisabled(!$can_edit)
+          ->setWorkflow(true));
+    } else {
+      $list->addAction(
+        id(new PhabricatorActionView())
+          ->setName(pht('Archive Space'))
+          ->setIcon('fa-ban')
+          ->setHref($this->getApplicationURI("archive/{$id}/"))
+          ->setDisabled(!$can_edit)
+          ->setWorkflow(true));
+    }
+
     return $list;
   }
 
 }
diff --git a/src/applications/spaces/editor/PhabricatorSpacesNamespaceEditor.php b/src/applications/spaces/editor/PhabricatorSpacesNamespaceEditor.php
index 23b01531e..caa45f28c 100644
--- a/src/applications/spaces/editor/PhabricatorSpacesNamespaceEditor.php
+++ b/src/applications/spaces/editor/PhabricatorSpacesNamespaceEditor.php
@@ -1,132 +1,166 @@
 <?php
 
 final class PhabricatorSpacesNamespaceEditor
   extends PhabricatorApplicationTransactionEditor {
 
   public function getEditorApplicationClass() {
     return pht('PhabricatorSpacesApplication');
   }
 
   public function getEditorObjectsDescription() {
     return pht('Spaces');
   }
 
   public function getTransactionTypes() {
     $types = parent::getTransactionTypes();
 
     $types[] = PhabricatorSpacesNamespaceTransaction::TYPE_NAME;
+    $types[] = PhabricatorSpacesNamespaceTransaction::TYPE_DESCRIPTION;
     $types[] = PhabricatorSpacesNamespaceTransaction::TYPE_DEFAULT;
+    $types[] = PhabricatorSpacesNamespaceTransaction::TYPE_ARCHIVE;
 
     $types[] = PhabricatorTransactions::TYPE_VIEW_POLICY;
     $types[] = PhabricatorTransactions::TYPE_EDIT_POLICY;
 
     return $types;
   }
 
   protected function getCustomTransactionOldValue(
     PhabricatorLiskDAO $object,
     PhabricatorApplicationTransaction $xaction) {
 
     switch ($xaction->getTransactionType()) {
       case PhabricatorSpacesNamespaceTransaction::TYPE_NAME:
         $name = $object->getNamespaceName();
         if (!strlen($name)) {
           return null;
         }
         return $name;
+      case PhabricatorSpacesNamespaceTransaction::TYPE_DESCRIPTION:
+        if ($this->getIsNewObject()) {
+          return null;
+        }
+        return $object->getDescription();
+      case PhabricatorSpacesNamespaceTransaction::TYPE_ARCHIVE:
+        return $object->getIsArchived();
       case PhabricatorSpacesNamespaceTransaction::TYPE_DEFAULT:
         return $object->getIsDefaultNamespace() ? 1 : null;
       case PhabricatorTransactions::TYPE_VIEW_POLICY:
         return $object->getViewPolicy();
       case PhabricatorTransactions::TYPE_EDIT_POLICY:
         return $object->getEditPolicy();
     }
 
     return parent::getCustomTransactionOldValue($object, $xaction);
   }
 
   protected function getCustomTransactionNewValue(
     PhabricatorLiskDAO $object,
     PhabricatorApplicationTransaction $xaction) {
 
     switch ($xaction->getTransactionType()) {
       case PhabricatorSpacesNamespaceTransaction::TYPE_NAME:
+      case PhabricatorSpacesNamespaceTransaction::TYPE_DESCRIPTION:
       case PhabricatorTransactions::TYPE_VIEW_POLICY:
       case PhabricatorTransactions::TYPE_EDIT_POLICY:
         return $xaction->getNewValue();
+      case PhabricatorSpacesNamespaceTransaction::TYPE_ARCHIVE:
+        return $xaction->getNewValue() ? 1 : 0;
       case PhabricatorSpacesNamespaceTransaction::TYPE_DEFAULT:
         return $xaction->getNewValue() ? 1 : null;
     }
 
     return parent::getCustomTransactionNewValue($object, $xaction);
   }
 
   protected function applyCustomInternalTransaction(
     PhabricatorLiskDAO $object,
     PhabricatorApplicationTransaction $xaction) {
 
     $new = $xaction->getNewValue();
 
     switch ($xaction->getTransactionType()) {
       case PhabricatorSpacesNamespaceTransaction::TYPE_NAME:
         $object->setNamespaceName($new);
         return;
+      case PhabricatorSpacesNamespaceTransaction::TYPE_DESCRIPTION:
+        $object->setDescription($new);
+        return;
       case PhabricatorSpacesNamespaceTransaction::TYPE_DEFAULT:
         $object->setIsDefaultNamespace($new ? 1 : null);
         return;
+      case PhabricatorSpacesNamespaceTransaction::TYPE_ARCHIVE:
+        $object->setIsArchived($new ? 1 : 0);
+        return;
       case PhabricatorTransactions::TYPE_VIEW_POLICY:
         $object->setViewPolicy($new);
         return;
       case PhabricatorTransactions::TYPE_EDIT_POLICY:
         $object->setEditPolicy($new);
         return;
     }
 
     return parent::applyCustomInternalTransaction($object, $xaction);
   }
 
   protected function applyCustomExternalTransaction(
     PhabricatorLiskDAO $object,
     PhabricatorApplicationTransaction $xaction) {
 
     switch ($xaction->getTransactionType()) {
       case PhabricatorSpacesNamespaceTransaction::TYPE_NAME:
+      case PhabricatorSpacesNamespaceTransaction::TYPE_DESCRIPTION:
       case PhabricatorSpacesNamespaceTransaction::TYPE_DEFAULT:
+      case PhabricatorSpacesNamespaceTransaction::TYPE_ARCHIVE:
       case PhabricatorTransactions::TYPE_VIEW_POLICY:
       case PhabricatorTransactions::TYPE_EDIT_POLICY:
         return;
     }
 
     return parent::applyCustomExternalTransaction($object, $xaction);
   }
 
   protected function validateTransaction(
     PhabricatorLiskDAO $object,
     $type,
     array $xactions) {
 
     $errors = parent::validateTransaction($object, $type, $xactions);
 
     switch ($type) {
       case PhabricatorSpacesNamespaceTransaction::TYPE_NAME:
         $missing = $this->validateIsEmptyTextField(
           $object->getNamespaceName(),
           $xactions);
 
         if ($missing) {
           $error = new PhabricatorApplicationTransactionValidationError(
             $type,
             pht('Required'),
-              pht('Spaces must have a name.'),
+            pht('Spaces must have a name.'),
             nonempty(last($xactions), null));
 
           $error->setIsMissingFieldError(true);
           $errors[] = $error;
         }
         break;
+      case PhabricatorSpacesNamespaceTransaction::TYPE_DEFAULT:
+        if (!$this->getIsNewObject()) {
+          foreach ($xactions as $xaction) {
+            $errors[] = new PhabricatorApplicationTransactionValidationError(
+              $type,
+              pht('Invalid'),
+              pht(
+                'Only the first space created can be the default space, and '.
+                'it must remain the default space evermore.'),
+              $xaction);
+          }
+        }
+        break;
+
     }
 
     return $errors;
   }
 
 }
diff --git a/src/applications/spaces/phid/PhabricatorSpacesNamespacePHIDType.php b/src/applications/spaces/phid/PhabricatorSpacesNamespacePHIDType.php
index 22dfcffac..6645c7edb 100644
--- a/src/applications/spaces/phid/PhabricatorSpacesNamespacePHIDType.php
+++ b/src/applications/spaces/phid/PhabricatorSpacesNamespacePHIDType.php
@@ -1,71 +1,78 @@
 <?php
 
 final class PhabricatorSpacesNamespacePHIDType
   extends PhabricatorPHIDType {
 
   const TYPECONST = 'SPCE';
 
   public function getTypeName() {
     return pht('Space');
   }
 
   public function getPHIDTypeApplicationClass() {
     return 'PhabricatorSpacesApplication';
   }
 
   public function newObject() {
     return new PhabricatorSpacesNamespace();
   }
 
   protected function buildQueryForObjects(
     PhabricatorObjectQuery $query,
     array $phids) {
 
     return id(new PhabricatorSpacesNamespaceQuery())
       ->withPHIDs($phids);
   }
 
   public function loadHandles(
     PhabricatorHandleQuery $query,
     array $handles,
     array $objects) {
 
     foreach ($handles as $phid => $handle) {
       $namespace = $objects[$phid];
+
       $monogram = $namespace->getMonogram();
+      $name = $namespace->getNamespaceName();
 
-      $handle->setName($namespace->getNamespaceName());
+      $handle->setName($name);
+      $handle->setFullName(pht('%s %s', $monogram, $name));
       $handle->setURI('/'.$monogram);
+
+      if ($namespace->getIsArchived()) {
+        $handle->setStatus(PhabricatorObjectHandle::STATUS_CLOSED);
+      }
     }
   }
 
   public function canLoadNamedObject($name) {
     return preg_match('/^S[1-9]\d*$/i', $name);
   }
 
   public function loadNamedObjects(
     PhabricatorObjectQuery $query,
     array $names) {
 
     $id_map = array();
     foreach ($names as $name) {
       $id = (int)substr($name, 1);
       $id_map[$id][] = $name;
     }
 
     $objects = id(new PhabricatorSpacesNamespaceQuery())
       ->setViewer($query->getViewer())
       ->withIDs(array_keys($id_map))
       ->execute();
 
     $results = array();
     foreach ($objects as $id => $object) {
       foreach (idx($id_map, $id, array()) as $name) {
         $results[$name] = $object;
       }
     }
 
     return $results;
   }
 
 }
diff --git a/src/applications/spaces/query/PhabricatorSpacesNamespaceQuery.php b/src/applications/spaces/query/PhabricatorSpacesNamespaceQuery.php
index ac02c306e..65ac146bd 100644
--- a/src/applications/spaces/query/PhabricatorSpacesNamespaceQuery.php
+++ b/src/applications/spaces/query/PhabricatorSpacesNamespaceQuery.php
@@ -1,177 +1,229 @@
 <?php
 
 final class PhabricatorSpacesNamespaceQuery
   extends PhabricatorCursorPagedPolicyAwareQuery {
 
   const KEY_ALL = 'spaces.all';
   const KEY_DEFAULT = 'spaces.default';
 
   private $ids;
   private $phids;
   private $isDefaultNamespace;
+  private $isArchived;
 
   public function withIDs(array $ids) {
     $this->ids = $ids;
     return $this;
   }
 
   public function withPHIDs(array $phids) {
     $this->phids = $phids;
     return $this;
   }
 
   public function withIsDefaultNamespace($default) {
     $this->isDefaultNamespace = $default;
     return $this;
   }
 
+  public function withIsArchived($archived) {
+    $this->isArchived = $archived;
+    return $this;
+  }
+
   public function getQueryApplicationClass() {
     return 'PhabricatorSpacesApplication';
   }
 
   protected function loadPage() {
-    $table = new PhabricatorSpacesNamespace();
-    $conn_r = $table->establishConnection('r');
-
-    $rows = 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($rows);
+    return $this->loadStandardPage(new PhabricatorSpacesNamespace());
   }
 
-  protected function buildWhereClause(AphrontDatabaseConnection $conn_r) {
-    $where = array();
+  protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) {
+    $where = parent::buildWhereClauseParts($conn);
 
     if ($this->ids !== null) {
       $where[] = qsprintf(
-        $conn_r,
+        $conn,
         'id IN (%Ld)',
         $this->ids);
     }
 
     if ($this->phids !== null) {
       $where[] = qsprintf(
-        $conn_r,
+        $conn,
         'phid IN (%Ls)',
         $this->phids);
     }
 
     if ($this->isDefaultNamespace !== null) {
       if ($this->isDefaultNamespace) {
         $where[] = qsprintf(
-          $conn_r,
+          $conn,
           'isDefaultNamespace = 1');
       } else {
         $where[] = qsprintf(
-          $conn_r,
+          $conn,
           'isDefaultNamespace IS NULL');
       }
     }
 
-    $where[] = $this->buildPagingClause($conn_r);
-    return $this->formatWhereClause($where);
+    if ($this->isArchived !== null) {
+      $where[] = qsprintf(
+        $conn,
+        'isArchived = %d',
+        (int)$this->isArchived);
+    }
+
+    return $where;
   }
 
   public static function destroySpacesCache() {
     $cache = PhabricatorCaches::getRequestCache();
     $cache->deleteKeys(
       array(
         self::KEY_ALL,
         self::KEY_DEFAULT,
       ));
   }
 
   public static function getSpacesExist() {
     return (bool)self::getAllSpaces();
   }
 
+  public static function getViewerSpacesExist(PhabricatorUser $viewer) {
+    if (!self::getSpacesExist()) {
+      return false;
+    }
+
+    // If the viewer has access to only one space, pretend spaces simply don't
+    // exist.
+    $spaces = self::getViewerSpaces($viewer);
+    return (count($spaces) > 1);
+  }
+
   public static function getAllSpaces() {
     $cache = PhabricatorCaches::getRequestCache();
     $cache_key = self::KEY_ALL;
 
     $spaces = $cache->getKey($cache_key);
     if ($spaces === null) {
       $spaces = id(new PhabricatorSpacesNamespaceQuery())
         ->setViewer(PhabricatorUser::getOmnipotentUser())
         ->execute();
       $spaces = mpull($spaces, null, 'getPHID');
       $cache->setKey($cache_key, $spaces);
     }
 
     return $spaces;
   }
 
   public static function getDefaultSpace() {
     $cache = PhabricatorCaches::getRequestCache();
     $cache_key = self::KEY_DEFAULT;
 
     $default_space = $cache->getKey($cache_key, false);
     if ($default_space === false) {
       $default_space = null;
 
       $spaces = self::getAllSpaces();
       foreach ($spaces as $space) {
         if ($space->getIsDefaultNamespace()) {
           $default_space = $space;
           break;
         }
       }
 
       $cache->setKey($cache_key, $default_space);
     }
 
     return $default_space;
   }
 
   public static function getViewerSpaces(PhabricatorUser $viewer) {
     $spaces = self::getAllSpaces();
 
     $result = array();
     foreach ($spaces as $key => $space) {
       $can_see = PhabricatorPolicyFilter::hasCapability(
         $viewer,
         $space,
         PhabricatorPolicyCapability::CAN_VIEW);
       if ($can_see) {
         $result[$key] = $space;
       }
     }
 
     return $result;
   }
 
+
+  public static function getViewerActiveSpaces(PhabricatorUser $viewer) {
+    $spaces = self::getViewerSpaces($viewer);
+
+    foreach ($spaces as $key => $space) {
+      if ($space->getIsArchived()) {
+        unset($spaces[$key]);
+      }
+    }
+
+    return $spaces;
+  }
+
+  public static function getSpaceOptionsForViewer(
+    PhabricatorUser $viewer,
+    $space_phid) {
+
+    $viewer_spaces = self::getViewerSpaces($viewer);
+
+    $map = array();
+    foreach ($viewer_spaces as $space) {
+
+      // Skip archived spaces, unless the object is already in that space.
+      if ($space->getIsArchived()) {
+        if ($space->getPHID() != $space_phid) {
+          continue;
+        }
+      }
+
+      $map[$space->getPHID()] = pht(
+        'Space %s: %s',
+        $space->getMonogram(),
+        $space->getNamespaceName());
+    }
+    asort($map);
+
+    return $map;
+  }
+
+
   /**
    * Get the Space PHID for an object, if one exists.
    *
    * This is intended to simplify performing a bunch of redundant checks; you
    * can intentionally pass any value in (including `null`).
    *
    * @param wild
    * @return phid|null
    */
   public static function getObjectSpacePHID($object) {
     if (!$object) {
       return null;
     }
 
     if (!($object instanceof PhabricatorSpacesInterface)) {
       return null;
     }
 
     $space_phid = $object->getSpacePHID();
     if ($space_phid === null) {
       $default_space = self::getDefaultSpace();
       if ($default_space) {
         $space_phid = $default_space->getPHID();
       }
     }
 
     return $space_phid;
   }
 
 }
diff --git a/src/applications/spaces/query/PhabricatorSpacesNamespaceSearchEngine.php b/src/applications/spaces/query/PhabricatorSpacesNamespaceSearchEngine.php
index 9b4014c3d..6a7ea7ca4 100644
--- a/src/applications/spaces/query/PhabricatorSpacesNamespaceSearchEngine.php
+++ b/src/applications/spaces/query/PhabricatorSpacesNamespaceSearchEngine.php
@@ -1,81 +1,97 @@
 <?php
 
 final class PhabricatorSpacesNamespaceSearchEngine
   extends PhabricatorApplicationSearchEngine {
 
   public function getApplicationClassName() {
     return 'PhabricatorSpacesApplication';
   }
 
   public function getResultTypeDescription() {
     return pht('Spaces');
   }
 
-  public function buildSavedQueryFromRequest(AphrontRequest $request) {
-    $saved = new PhabricatorSavedQuery();
+  public function newQuery() {
+    return new PhabricatorSpacesNamespaceQuery();
+  }
 
-    return $saved;
+  protected function buildCustomSearchFields() {
+    return array(
+      id(new PhabricatorSearchThreeStateField())
+        ->setLabel(pht('Active'))
+        ->setKey('active')
+        ->setOptions(
+          pht('(Show All)'),
+          pht('Show Only Active Spaces'),
+          pht('Hide Active Spaces')),
+    );
   }
 
-  public function buildQueryFromSavedQuery(PhabricatorSavedQuery $saved) {
-    $query = id(new PhabricatorSpacesNamespaceQuery());
+  protected function buildQueryFromParameters(array $map) {
+    $query = $this->newQuery();
+
+    if ($map['active']) {
+      $query->withIsArchived(!$map['active']);
+    }
 
     return $query;
   }
 
-  public function buildSearchForm(
-    AphrontFormView $form,
-    PhabricatorSavedQuery $saved_query) {}
-
   protected function getURI($path) {
     return '/spaces/'.$path;
   }
 
   protected function getBuiltinQueryNames() {
     $names = array(
+      'active' => pht('Active Spaces'),
       'all' => pht('All Spaces'),
     );
 
     return $names;
   }
 
   public function buildSavedQueryFromBuiltin($query_key) {
-
     $query = $this->newSavedQuery();
     $query->setQueryKey($query_key);
 
     switch ($query_key) {
+      case 'active':
+        return $query->setParameter('active', true);
       case 'all':
         return $query;
     }
 
     return parent::buildSavedQueryFromBuiltin($query_key);
   }
 
   protected function renderResultList(
     array $spaces,
     PhabricatorSavedQuery $query,
     array $handles) {
     assert_instances_of($spaces, 'PhabricatorSpacesNamespace');
 
     $viewer = $this->requireViewer();
 
     $list = new PHUIObjectItemListView();
     $list->setUser($viewer);
     foreach ($spaces as $space) {
       $item = id(new PHUIObjectItemView())
         ->setObjectName($space->getMonogram())
         ->setHeader($space->getNamespaceName())
         ->setHref('/'.$space->getMonogram());
 
       if ($space->getIsDefaultNamespace()) {
         $item->addIcon('fa-certificate', pht('Default Space'));
       }
 
+      if ($space->getIsArchived()) {
+        $item->setDisabled(true);
+      }
+
       $list->addItem($item);
     }
 
     return $list;
   }
 
 }
diff --git a/src/applications/spaces/storage/PhabricatorSpacesNamespace.php b/src/applications/spaces/storage/PhabricatorSpacesNamespace.php
index 8271f536c..f728d22df 100644
--- a/src/applications/spaces/storage/PhabricatorSpacesNamespace.php
+++ b/src/applications/spaces/storage/PhabricatorSpacesNamespace.php
@@ -1,116 +1,122 @@
 <?php
 
 final class PhabricatorSpacesNamespace
   extends PhabricatorSpacesDAO
   implements
     PhabricatorPolicyInterface,
     PhabricatorApplicationTransactionInterface,
     PhabricatorDestructibleInterface {
 
   protected $namespaceName;
   protected $viewPolicy;
   protected $editPolicy;
   protected $isDefaultNamespace;
+  protected $description;
+  protected $isArchived;
 
   public static function initializeNewNamespace(PhabricatorUser $actor) {
     $app = id(new PhabricatorApplicationQuery())
       ->setViewer($actor)
       ->withClasses(array('PhabricatorSpacesApplication'))
       ->executeOne();
 
     $view_policy = $app->getPolicy(
       PhabricatorSpacesCapabilityDefaultView::CAPABILITY);
     $edit_policy = $app->getPolicy(
       PhabricatorSpacesCapabilityDefaultEdit::CAPABILITY);
 
     return id(new PhabricatorSpacesNamespace())
       ->setIsDefaultNamespace(null)
       ->setViewPolicy($view_policy)
-      ->setEditPolicy($edit_policy);
+      ->setEditPolicy($edit_policy)
+      ->setDescription('')
+      ->setIsArchived(0);
   }
 
   protected function getConfiguration() {
     return array(
       self::CONFIG_AUX_PHID => true,
       self::CONFIG_COLUMN_SCHEMA => array(
         'namespaceName' => 'text255',
         'isDefaultNamespace' => 'bool?',
+        'description' => 'text',
+        'isArchived' => 'bool',
       ),
       self::CONFIG_KEY_SCHEMA => array(
         'key_default' => array(
           'columns' => array('isDefaultNamespace'),
           'unique' => true,
         ),
       ),
     ) + parent::getConfiguration();
   }
 
   public function generatePHID() {
     return PhabricatorPHID::generateNewPHID(
       PhabricatorSpacesNamespacePHIDType::TYPECONST);
   }
 
   public function getMonogram() {
     return 'S'.$this->getID();
   }
 
 
 /* -(  PhabricatorPolicyInterface  )----------------------------------------- */
 
 
   public function getCapabilities() {
     return array(
       PhabricatorPolicyCapability::CAN_VIEW,
       PhabricatorPolicyCapability::CAN_EDIT,
     );
   }
 
   public function getPolicy($capability) {
     switch ($capability) {
       case PhabricatorPolicyCapability::CAN_VIEW:
         return $this->getViewPolicy();
       case PhabricatorPolicyCapability::CAN_EDIT:
         return $this->getEditPolicy();
     }
   }
 
   public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
     return false;
   }
 
   public function describeAutomaticCapability($capability) {
     return null;
   }
 
 
 /* -(  PhabricatorApplicationTransactionInterface  )------------------------- */
 
 
   public function getApplicationTransactionEditor() {
     return new PhabricatorSpacesNamespaceEditor();
   }
 
   public function getApplicationTransactionObject() {
     return $this;
   }
 
   public function getApplicationTransactionTemplate() {
     return new PhabricatorSpacesNamespaceTransaction();
   }
 
   public function willRenderTimeline(
     PhabricatorApplicationTransactionView $timeline,
     AphrontRequest $request) {
     return $timeline;
   }
 
 
 /* -(  PhabricatorDestructibleInterface  )----------------------------------- */
 
 
   public function destroyObjectPermanently(
     PhabricatorDestructionEngine $engine) {
     $this->delete();
   }
 
 }
diff --git a/src/applications/spaces/storage/PhabricatorSpacesNamespaceTransaction.php b/src/applications/spaces/storage/PhabricatorSpacesNamespaceTransaction.php
index 672cb9d8a..4c438537f 100644
--- a/src/applications/spaces/storage/PhabricatorSpacesNamespaceTransaction.php
+++ b/src/applications/spaces/storage/PhabricatorSpacesNamespaceTransaction.php
@@ -1,49 +1,97 @@
 <?php
 
 final class PhabricatorSpacesNamespaceTransaction
   extends PhabricatorApplicationTransaction {
 
   const TYPE_NAME = 'spaces:name';
   const TYPE_DEFAULT = 'spaces:default';
+  const TYPE_DESCRIPTION = 'spaces:description';
+  const TYPE_ARCHIVE = 'spaces:archive';
 
   public function getApplicationName() {
     return 'spaces';
   }
 
   public function getApplicationTransactionType() {
     return PhabricatorSpacesNamespacePHIDType::TYPECONST;
   }
 
   public function getApplicationTransactionCommentObject() {
     return null;
   }
 
+  public function shouldHide() {
+    $old = $this->getOldValue();
+
+    switch ($this->getTransactionType()) {
+      case self::TYPE_DESCRIPTION:
+        return ($old === null);
+    }
+
+    return parent::shouldHide();
+  }
+
+  public function hasChangeDetails() {
+    switch ($this->getTransactionType()) {
+      case self::TYPE_DESCRIPTION:
+        return true;
+    }
+
+    return parent::hasChangeDetails();
+  }
+
+  public function getRemarkupBlocks() {
+    $blocks = parent::getRemarkupBlocks();
+
+    switch ($this->getTransactionType()) {
+      case self::TYPE_DESCRIPTION:
+        $blocks[] = $this->getNewValue();
+        break;
+    }
+
+    return $blocks;
+  }
+
   public function getTitle() {
     $old = $this->getOldValue();
     $new = $this->getNewValue();
 
     $author_phid = $this->getAuthorPHID();
 
     switch ($this->getTransactionType()) {
       case self::TYPE_NAME:
         if ($old === null) {
           return pht(
             '%s created this space.',
             $this->renderHandleLink($author_phid));
         } else {
           return pht(
             '%s renamed this space from "%s" to "%s".',
             $this->renderHandleLink($author_phid),
             $old,
             $new);
         }
+      case self::TYPE_DESCRIPTION:
+        return pht(
+          '%s updated the description for this space.',
+            $this->renderHandleLink($author_phid));
       case self::TYPE_DEFAULT:
         return pht(
           '%s made this the default space.',
           $this->renderHandleLink($author_phid));
+      case self::TYPE_ARCHIVE:
+        if ($new) {
+          return pht(
+            '%s archived this space.',
+            $this->renderHandleLink($author_phid));
+        } else {
+          return pht(
+            '%s activated this space.',
+            $this->renderHandleLink($author_phid));
+        }
     }
 
     return parent::getTitle();
   }
 
 }
diff --git a/src/applications/spaces/storage/PhabricatorSpacesSchemaSpec.php b/src/applications/spaces/storage/PhabricatorSpacesSchemaSpec.php
new file mode 100644
index 000000000..d69772ef7
--- /dev/null
+++ b/src/applications/spaces/storage/PhabricatorSpacesSchemaSpec.php
@@ -0,0 +1,10 @@
+<?php
+
+final class PhabricatorSpacesSchemaSpec
+  extends PhabricatorConfigSchemaSpec {
+
+  public function buildSchemata() {
+    $this->buildEdgeSchemata(new PhabricatorSpacesNamespace());
+  }
+
+}
diff --git a/src/applications/spaces/typeahead/PhabricatorSpacesNamespaceDatasource.php b/src/applications/spaces/typeahead/PhabricatorSpacesNamespaceDatasource.php
index 046e3b0f4..25951b53a 100644
--- a/src/applications/spaces/typeahead/PhabricatorSpacesNamespaceDatasource.php
+++ b/src/applications/spaces/typeahead/PhabricatorSpacesNamespaceDatasource.php
@@ -1,32 +1,43 @@
 <?php
 
 final class PhabricatorSpacesNamespaceDatasource
   extends PhabricatorTypeaheadDatasource {
 
   public function getBrowseTitle() {
     return pht('Browse Spaces');
   }
 
   public function getPlaceholderText() {
     return pht('Type a space name...');
   }
 
   public function getDatasourceApplicationClass() {
     return 'PhabricatorSpacesApplication';
   }
 
   public function loadResults() {
     $query = id(new PhabricatorSpacesNamespaceQuery());
 
     $spaces = $this->executeQuery($query);
     $results = array();
     foreach ($spaces as $space) {
-      $results[] = id(new PhabricatorTypeaheadResult())
-        ->setName($space->getNamespaceName())
+      $full_name = pht(
+        '%s %s',
+        $space->getMonogram(),
+        $space->getNamespaceName());
+
+      $result = id(new PhabricatorTypeaheadResult())
+        ->setName($full_name)
         ->setPHID($space->getPHID());
+
+      if ($space->getIsArchived()) {
+        $result->setClosed(pht('Archived'));
+      }
+
+      $results[] = $result;
     }
 
     return $this->filterResultsAgainstTokens($results);
   }
 
 }
diff --git a/src/applications/spaces/view/PHUISpacesNamespaceContextView.php b/src/applications/spaces/view/PHUISpacesNamespaceContextView.php
index ed28f5334..0bb5949b7 100644
--- a/src/applications/spaces/view/PHUISpacesNamespaceContextView.php
+++ b/src/applications/spaces/view/PHUISpacesNamespaceContextView.php
@@ -1,36 +1,49 @@
 <?php
 
 final class PHUISpacesNamespaceContextView extends AphrontView {
 
   private $object;
 
   public function setObject($object) {
     $this->object = $object;
     return $this;
   }
 
   public function getObject() {
     return $this->object;
   }
 
   public function render() {
     $object = $this->getObject();
 
     $space_phid = PhabricatorSpacesNamespaceQuery::getObjectSpacePHID($object);
     if (!$space_phid) {
       return null;
     }
 
+    // If the viewer can't see spaces, pretend they don't exist.
     $viewer = $this->getUser();
+    if (!PhabricatorSpacesNamespaceQuery::getViewerSpacesExist($viewer)) {
+      return null;
+    }
+
+    // If this is the default space, don't show a space label.
+    $default = PhabricatorSpacesNamespaceQuery::getDefaultSpace();
+    if ($default) {
+      if ($default->getPHID() == $space_phid) {
+        return null;
+      }
+    }
+
     return phutil_tag(
       'span',
       array(
         'class' => 'spaces-name',
       ),
       array(
-        $viewer->renderHandle($space_phid),
+        $viewer->renderHandle($space_phid)->setUseShortName(true),
         ' | ',
       ));
   }
 
 }
diff --git a/src/applications/spaces/view/PhabricatorSpacesControl.php b/src/applications/spaces/view/PhabricatorSpacesControl.php
deleted file mode 100644
index aa40238c5..000000000
--- a/src/applications/spaces/view/PhabricatorSpacesControl.php
+++ /dev/null
@@ -1,49 +0,0 @@
-<?php
-
-final class PhabricatorSpacesControl extends AphrontFormControl {
-
-  private $object;
-
-  protected function shouldRender() {
-    // Render this control only if some Spaces exist.
-    return PhabricatorSpacesNamespaceQuery::getAllSpaces();
-  }
-
-  public function setObject(PhabricatorSpacesInterface $object) {
-    $this->object = $object;
-    return $this;
-  }
-
-  protected function getCustomControlClass() {
-    return '';
-  }
-
-  protected function getOptions() {
-    $viewer = $this->getUser();
-    $viewer_spaces = PhabricatorSpacesNamespaceQuery::getViewerSpaces($viewer);
-
-    $map = mpull($viewer_spaces, 'getNamespaceName', 'getPHID');
-    asort($map);
-
-    return $map;
-  }
-
-  protected function renderInput() {
-    $viewer = $this->getUser();
-
-    $this->setLabel(pht('Space'));
-
-    $value = $this->getValue();
-    if ($value === null) {
-      $value = $viewer->getDefaultSpacePHID();
-    }
-
-    return AphrontFormSelectControl::renderSelectTag(
-      $value,
-      $this->getOptions(),
-      array(
-        'name' => $this->getName(),
-      ));
-  }
-
-}
diff --git a/src/applications/subscriptions/policyrule/PhabricatorSubscriptionsSubscribersPolicyRule.php b/src/applications/subscriptions/policyrule/PhabricatorSubscriptionsSubscribersPolicyRule.php
new file mode 100644
index 000000000..7da35f370
--- /dev/null
+++ b/src/applications/subscriptions/policyrule/PhabricatorSubscriptionsSubscribersPolicyRule.php
@@ -0,0 +1,121 @@
+<?php
+
+final class PhabricatorSubscriptionsSubscribersPolicyRule
+  extends PhabricatorPolicyRule {
+
+  private $subscribed = array();
+  private $sourcePHIDs = array();
+
+  public function getObjectPolicyKey() {
+    return 'subscriptions.subscribers';
+  }
+
+  public function getObjectPolicyName() {
+    return pht('Subscribers');
+  }
+
+  public function getPolicyExplanation() {
+    return pht('Subscribers can take this action.');
+  }
+
+  public function getRuleDescription() {
+    return pht('subscribers');
+  }
+
+  public function canApplyToObject(PhabricatorPolicyInterface $object) {
+    return ($object instanceof PhabricatorSubscribableInterface);
+  }
+
+  public function willApplyRules(
+    PhabricatorUser $viewer,
+    array $values,
+    array $objects) {
+
+    // We want to let the user see the object if they're a subscriber or
+    // a member of any project which is a subscriber. Additionally, because
+    // subscriber state is complex, we need to read hints passed from
+    // the TransactionEditor to predict policy state after transactions apply.
+
+    $viewer_phid = $viewer->getPHID();
+    if (!$viewer_phid) {
+      return;
+    }
+
+    if (empty($this->subscribed[$viewer_phid])) {
+      $this->subscribed[$viewer_phid] = array();
+    }
+
+    // Load the project PHIDs the user is a member of.
+    if (!isset($this->sourcePHIDs[$viewer_phid])) {
+      $source_phids = PhabricatorEdgeQuery::loadDestinationPHIDs(
+        $viewer_phid,
+        PhabricatorProjectMemberOfProjectEdgeType::EDGECONST);
+      $source_phids[] = $viewer_phid;
+      $this->sourcePHIDs[$viewer_phid] = $source_phids;
+    }
+
+    // Look for transaction hints.
+    foreach ($objects as $key => $object) {
+      $cache = $this->getTransactionHint($object);
+      if ($cache === null) {
+        // We don't have a hint for this object, so we'll deal with it below.
+        continue;
+      }
+
+      // We have a hint, so use that as the source of truth.
+      unset($objects[$key]);
+
+      foreach ($this->sourcePHIDs[$viewer_phid] as $source_phid) {
+        if (isset($cache[$source_phid])) {
+          $this->subscribed[$viewer_phid][$object->getPHID()] = true;
+          break;
+        }
+      }
+    }
+
+    $phids = mpull($objects, 'getPHID');
+    if (!$phids) {
+      return;
+    }
+
+    $edge_query = id(new PhabricatorEdgeQuery())
+      ->withSourcePHIDs($this->sourcePHIDs[$viewer_phid])
+      ->withEdgeTypes(
+        array(
+          PhabricatorSubscribedToObjectEdgeType::EDGECONST,
+        ))
+      ->withDestinationPHIDs($phids);
+
+    $edge_query->execute();
+
+    $subscribed = $edge_query->getDestinationPHIDs();
+    if (!$subscribed) {
+      return;
+    }
+
+    $this->subscribed[$viewer_phid] += array_fill_keys($subscribed, true);
+  }
+
+  public function applyRule(
+    PhabricatorUser $viewer,
+    $value,
+    PhabricatorPolicyInterface $object) {
+
+    $viewer_phid = $viewer->getPHID();
+    if (!$viewer_phid) {
+      return false;
+    }
+
+    if ($object->isAutomaticallySubscribed($viewer_phid)) {
+      return true;
+    }
+
+    $subscribed = idx($this->subscribed, $viewer_phid);
+    return isset($subscribed[$object->getPHID()]);
+  }
+
+  public function getValueControlType() {
+    return self::CONTROL_TYPE_NONE;
+  }
+
+}
diff --git a/src/applications/subscriptions/view/SubscriptionListDialogBuilder.php b/src/applications/subscriptions/view/SubscriptionListDialogBuilder.php
index 67e013d9c..a4d382e17 100644
--- a/src/applications/subscriptions/view/SubscriptionListDialogBuilder.php
+++ b/src/applications/subscriptions/view/SubscriptionListDialogBuilder.php
@@ -1,82 +1,82 @@
 <?php
 
-final class SubscriptionListDialogBuilder {
+final class SubscriptionListDialogBuilder extends Phobject {
 
   private $viewer;
   private $handles;
   private $objectPHID;
   private $title;
 
   public function setViewer(PhabricatorUser $viewer) {
     $this->viewer = $viewer;
     return $this;
   }
 
   public function getViewer() {
     return $this->viewer;
   }
 
   public function setHandles(array $handles) {
     assert_instances_of($handles, 'PhabricatorObjectHandle');
     $this->handles = $handles;
     return $this;
   }
 
   public function getHandles() {
     return $this->handles;
   }
 
   public function setObjectPHID($object_phid) {
     $this->objectPHID = $object_phid;
     return $this;
   }
 
   public function getObjectPHID() {
     return $this->objectPHID;
   }
 
   public function setTitle($title) {
     $this->title = $title;
     return $this;
   }
 
   public function getTitle() {
     return $this->title;
   }
 
   public function buildDialog() {
     $phid = $this->getObjectPHID();
     $handles = $this->getHandles();
     $object_handle = $handles[$phid];
     unset($handles[$phid]);
 
     return id(new AphrontDialogView())
       ->setUser($this->getViewer())
       ->setWidth(AphrontDialogView::WIDTH_FORM)
       ->setTitle($this->getTitle())
       ->appendChild($this->buildBody($this->getViewer(), $handles))
       ->addCancelButton($object_handle->getURI(), pht('Close'));
   }
 
   private function buildBody(PhabricatorUser $viewer, array $handles) {
 
     $list = id(new PHUIObjectItemListView())
       ->setUser($viewer)
       ->setFlush(true);
     foreach ($handles as $handle) {
       $item = id(new PHUIObjectItemView())
         ->setHeader($handle->getFullName())
         ->setHref($handle->getURI())
         ->setDisabled($handle->isDisabled());
 
       if ($handle->getImageURI()) {
         $item->setImageURI($handle->getImageURI());
       }
 
       $list->addItem($item);
     }
 
     return $list;
   }
 
 }
diff --git a/src/applications/subscriptions/view/SubscriptionListStringBuilder.php b/src/applications/subscriptions/view/SubscriptionListStringBuilder.php
index 7e96984d4..e8761e554 100644
--- a/src/applications/subscriptions/view/SubscriptionListStringBuilder.php
+++ b/src/applications/subscriptions/view/SubscriptionListStringBuilder.php
@@ -1,84 +1,84 @@
 <?php
 
-final class SubscriptionListStringBuilder {
+final class SubscriptionListStringBuilder extends Phobject {
 
   private $handles;
   private $objectPHID;
 
   public function setHandles(array $handles) {
     assert_instances_of($handles, 'PhabricatorObjectHandle');
     $this->handles = $handles;
     return $this;
   }
 
   public function getHandles() {
     return $this->handles;
   }
 
   public function setObjectPHID($object_phid) {
     $this->objectPHID = $object_phid;
     return $this;
   }
 
   public function getObjectPHID() {
     return $this->objectPHID;
   }
 
   public function buildTransactionString($change_type) {
     $handles = $this->getHandles();
     if (!$handles) {
       return;
     }
     $list_uri = '/subscriptions/transaction/'.
                 $change_type.'/'.
                 $this->getObjectPHID().'/';
     return $this->buildString($list_uri);
   }
 
   public function buildPropertyString() {
     $handles = $this->getHandles();
 
     if (!$handles) {
       return phutil_tag('em', array(), pht('None'));
     }
     $list_uri = '/subscriptions/list/'.$this->getObjectPHID().'/';
     return $this->buildString($list_uri);
   }
 
   private function buildString($list_uri) {
     $handles = $this->getHandles();
 
     // Always show this many subscribers.
     $show_count = 3;
     $subscribers_count = count($handles);
 
     // It looks a bit silly to render "a, b, c, and 1 other", since we could
     // have just put that other subscriber there in place of the "1 other"
     // link. Instead, render "a, b, c, d" in this case, and then when we get one
     // more render "a, b, c, and 2 others".
     if ($subscribers_count <= ($show_count + 1)) {
       return phutil_implode_html(', ', mpull($handles, 'renderLink'));
     }
 
     $show = array_slice($handles, 0, $show_count);
     $show = array_values($show);
 
     $not_shown_count = $subscribers_count - $show_count;
     $not_shown_txt = pht('%d other(s)', $not_shown_count);
     $not_shown_link = javelin_tag(
       'a',
       array(
         'href' => $list_uri,
         'sigil' => 'workflow',
       ),
       $not_shown_txt);
 
     return pht(
       '%s, %s, %s and %s',
       $show[0]->renderLink(),
       $show[1]->renderLink(),
       $show[2]->renderLink(),
       $not_shown_link);
   }
 
 }
diff --git a/src/applications/system/action/PhabricatorSystemAction.php b/src/applications/system/action/PhabricatorSystemAction.php
index 1afe74ceb..329824bac 100644
--- a/src/applications/system/action/PhabricatorSystemAction.php
+++ b/src/applications/system/action/PhabricatorSystemAction.php
@@ -1,40 +1,40 @@
 <?php
 
-abstract class PhabricatorSystemAction {
+abstract class PhabricatorSystemAction extends Phobject {
 
   abstract public function getActionConstant();
   abstract public function getScoreThreshold();
 
   public function shouldBlockActor($actor, $score) {
     return ($score > $this->getScoreThreshold());
   }
 
   public function getLimitExplanation() {
     return pht('You are performing too many actions too quickly.');
   }
 
   public function getRateExplanation($score) {
     return pht(
       'The maximum allowed rate for this action is %s. You are taking '.
       'actions at a rate of %s.',
       $this->formatRate($this->getScoreThreshold()),
       $this->formatRate($score));
   }
 
   protected function formatRate($rate) {
     if ($rate > 10) {
       $str = pht('%d / second', $rate);
     } else {
       $rate *= 60;
       if ($rate > 10) {
         $str = pht('%d / minute', $rate);
       } else {
         $rate *= 60;
         $str = pht('%d / hour', $rate);
       }
     }
 
     return phutil_tag('strong', array(), $str);
   }
 
 }
diff --git a/src/applications/transactions/constants/PhabricatorTransactions.php b/src/applications/transactions/constants/PhabricatorTransactions.php
index 6bdbd2f2d..1422f2108 100644
--- a/src/applications/transactions/constants/PhabricatorTransactions.php
+++ b/src/applications/transactions/constants/PhabricatorTransactions.php
@@ -1,38 +1,38 @@
 <?php
 
-final class PhabricatorTransactions {
+final class PhabricatorTransactions extends Phobject {
 
   const TYPE_COMMENT      = 'core:comment';
   const TYPE_SUBSCRIBERS  = 'core:subscribers';
   const TYPE_VIEW_POLICY  = 'core:view-policy';
   const TYPE_EDIT_POLICY  = 'core:edit-policy';
   const TYPE_JOIN_POLICY  = 'core:join-policy';
   const TYPE_EDGE         = 'core:edge';
   const TYPE_CUSTOMFIELD  = 'core:customfield';
   const TYPE_BUILDABLE    = 'harbormaster:buildable';
   const TYPE_TOKEN        = 'token:give';
   const TYPE_INLINESTATE  = 'core:inlinestate';
   const TYPE_SPACE = 'core:space';
 
   const COLOR_RED         = 'red';
   const COLOR_ORANGE      = 'orange';
   const COLOR_YELLOW      = 'yellow';
   const COLOR_GREEN       = 'green';
   const COLOR_SKY         = 'sky';
   const COLOR_BLUE        = 'blue';
   const COLOR_INDIGO      = 'indigo';
   const COLOR_VIOLET      = 'violet';
   const COLOR_GREY        = 'grey';
   const COLOR_BLACK       = 'black';
 
 
   public static function getInlineStateMap() {
     return array(
       PhabricatorInlineCommentInterface::STATE_DRAFT =>
         PhabricatorInlineCommentInterface::STATE_DONE,
       PhabricatorInlineCommentInterface::STATE_UNDRAFT =>
         PhabricatorInlineCommentInterface::STATE_UNDONE,
     );
   }
 
 }
diff --git a/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php b/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php
index d89c78290..6bf88b4e2 100644
--- a/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php
+++ b/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php
@@ -1,3063 +1,3125 @@
 <?php
 
 /**
  *
  * Publishing and Managing State
  * ======
  *
  * After applying changes, the Editor queues a worker to publish mail, feed,
  * and notifications, and to perform other background work like updating search
  * indexes. This allows it to do this work without impacting performance for
  * users.
  *
  * When work is moved to the daemons, the Editor state is serialized by
  * @{method:getWorkerState}, then reloaded in a daemon process by
  * @{method:loadWorkerState}. **This is fragile.**
  *
  * State is not persisted into the daemons by default, because we can not send
  * arbitrary objects into the queue. This means the default behavior of any
  * state properties is to reset to their defaults without warning prior to
  * publishing.
  *
  * The easiest way to avoid this is to keep Editors stateless: the overwhelming
  * majority of Editors can be written statelessly. If you need to maintain
  * state, you can either:
  *
  *   - not require state to exist during publishing; or
  *   - pass state to the daemons by implementing @{method:getCustomWorkerState}
  *     and @{method:loadCustomWorkerState}.
  *
  * This architecture isn't ideal, and we may eventually split this class into
  * "Editor" and "Publisher" parts to make it more robust. See T6367 for some
  * discussion and context.
  *
  * @task mail Sending Mail
  * @task feed Publishing Feed Stories
  * @task search Search Index
  * @task files Integration with Files
  * @task workers Managing Workers
  */
 abstract class PhabricatorApplicationTransactionEditor
   extends PhabricatorEditor {
 
   private $contentSource;
   private $object;
   private $xactions;
 
   private $isNewObject;
   private $mentionedPHIDs;
   private $continueOnNoEffect;
   private $continueOnMissingFields;
   private $parentMessageID;
   private $heraldAdapter;
   private $heraldTranscript;
   private $subscribers;
   private $unmentionablePHIDMap = array();
   private $applicationEmail;
 
   private $isPreview;
   private $isHeraldEditor;
   private $isInverseEdgeEditor;
   private $actingAsPHID;
   private $disableEmail;
 
   private $heraldEmailPHIDs = array();
   private $heraldForcedEmailPHIDs = array();
   private $heraldHeader;
   private $mailToPHIDs = array();
   private $mailCCPHIDs = array();
   private $feedNotifyPHIDs = array();
   private $feedRelatedPHIDs = array();
 
   /**
    * Get the class name for the application this editor is a part of.
    *
    * Uninstalling the application will disable the editor.
    *
    * @return string Editor's application class name.
    */
   abstract public function getEditorApplicationClass();
 
 
   /**
    * Get a description of the objects this editor edits, like "Differential
    * Revisions".
    *
    * @return string Human readable description of edited objects.
    */
   abstract public function getEditorObjectsDescription();
 
 
   public function setActingAsPHID($acting_as_phid) {
     $this->actingAsPHID = $acting_as_phid;
     return $this;
   }
 
   public function getActingAsPHID() {
     if ($this->actingAsPHID) {
       return $this->actingAsPHID;
     }
     return $this->getActor()->getPHID();
   }
 
 
   /**
    * When the editor tries to apply transactions that have no effect, should
    * it raise an exception (default) or drop them and continue?
    *
    * Generally, you will set this flag for edits coming from "Edit" interfaces,
    * and leave it cleared for edits coming from "Comment" interfaces, so the
    * user will get a useful error if they try to submit a comment that does
    * nothing (e.g., empty comment with a status change that has already been
    * performed by another user).
    *
    * @param bool  True to drop transactions without effect and continue.
    * @return this
    */
   public function setContinueOnNoEffect($continue) {
     $this->continueOnNoEffect = $continue;
     return $this;
   }
 
   public function getContinueOnNoEffect() {
     return $this->continueOnNoEffect;
   }
 
 
   /**
    * When the editor tries to apply transactions which don't populate all of
    * an object's required fields, should it raise an exception (default) or
    * drop them and continue?
    *
    * For example, if a user adds a new required custom field (like "Severity")
    * to a task, all existing tasks won't have it populated. When users
    * manually edit existing tasks, it's usually desirable to have them provide
    * a severity. However, other operations (like batch editing just the
    * owner of a task) will fail by default.
    *
    * By setting this flag for edit operations which apply to specific fields
    * (like the priority, batch, and merge editors in Maniphest), these
    * operations can continue to function even if an object is outdated.
    *
    * @param bool  True to continue when transactions don't completely satisfy
    *              all required fields.
    * @return this
    */
   public function setContinueOnMissingFields($continue_on_missing_fields) {
     $this->continueOnMissingFields = $continue_on_missing_fields;
     return $this;
   }
 
   public function getContinueOnMissingFields() {
     return $this->continueOnMissingFields;
   }
 
 
   /**
    * Not strictly necessary, but reply handlers ideally set this value to
    * make email threading work better.
    */
   public function setParentMessageID($parent_message_id) {
     $this->parentMessageID = $parent_message_id;
     return $this;
   }
   public function getParentMessageID() {
     return $this->parentMessageID;
   }
 
   public function getIsNewObject() {
     return $this->isNewObject;
   }
 
   protected function getMentionedPHIDs() {
     return $this->mentionedPHIDs;
   }
 
   public function setIsPreview($is_preview) {
     $this->isPreview = $is_preview;
     return $this;
   }
 
   public function getIsPreview() {
     return $this->isPreview;
   }
 
   public function setIsInverseEdgeEditor($is_inverse_edge_editor) {
     $this->isInverseEdgeEditor = $is_inverse_edge_editor;
     return $this;
   }
 
   public function getIsInverseEdgeEditor() {
     return $this->isInverseEdgeEditor;
   }
 
   public function setIsHeraldEditor($is_herald_editor) {
     $this->isHeraldEditor = $is_herald_editor;
     return $this;
   }
 
   public function getIsHeraldEditor() {
     return $this->isHeraldEditor;
   }
 
   /**
    * Prevent this editor from generating email when applying transactions.
    *
    * @param bool  True to disable email.
    * @return this
    */
   public function setDisableEmail($disable_email) {
     $this->disableEmail = $disable_email;
     return $this;
   }
 
   public function getDisableEmail() {
     return $this->disableEmail;
   }
 
   public function setUnmentionablePHIDMap(array $map) {
     $this->unmentionablePHIDMap = $map;
     return $this;
   }
 
   public function getUnmentionablePHIDMap() {
     return $this->unmentionablePHIDMap;
   }
 
   protected function shouldEnableMentions(
     PhabricatorLiskDAO $object,
     array $xactions) {
     return true;
   }
 
   public function setApplicationEmail(
     PhabricatorMetaMTAApplicationEmail $email) {
     $this->applicationEmail = $email;
     return $this;
   }
 
   public function getApplicationEmail() {
     return $this->applicationEmail;
   }
 
   public function getTransactionTypes() {
     $types = array();
 
     if ($this->object instanceof PhabricatorSubscribableInterface) {
       $types[] = PhabricatorTransactions::TYPE_SUBSCRIBERS;
     }
 
     if ($this->object instanceof PhabricatorCustomFieldInterface) {
       $types[] = PhabricatorTransactions::TYPE_CUSTOMFIELD;
     }
 
     if ($this->object instanceof HarbormasterBuildableInterface) {
       $types[] = PhabricatorTransactions::TYPE_BUILDABLE;
     }
 
     if ($this->object instanceof PhabricatorTokenReceiverInterface) {
       $types[] = PhabricatorTransactions::TYPE_TOKEN;
     }
 
     if ($this->object instanceof PhabricatorProjectInterface ||
         $this->object instanceof PhabricatorMentionableInterface) {
       $types[] = PhabricatorTransactions::TYPE_EDGE;
     }
 
     if ($this->object instanceof PhabricatorSpacesInterface) {
       $types[] = PhabricatorTransactions::TYPE_SPACE;
     }
 
     return $types;
   }
 
   private function adjustTransactionValues(
     PhabricatorLiskDAO $object,
     PhabricatorApplicationTransaction $xaction) {
 
     if ($xaction->shouldGenerateOldValue()) {
       $old = $this->getTransactionOldValue($object, $xaction);
       $xaction->setOldValue($old);
     }
 
     $new = $this->getTransactionNewValue($object, $xaction);
     $xaction->setNewValue($new);
   }
 
   private function getTransactionOldValue(
     PhabricatorLiskDAO $object,
     PhabricatorApplicationTransaction $xaction) {
     switch ($xaction->getTransactionType()) {
       case PhabricatorTransactions::TYPE_SUBSCRIBERS:
         return array_values($this->subscribers);
       case PhabricatorTransactions::TYPE_VIEW_POLICY:
         return $object->getViewPolicy();
       case PhabricatorTransactions::TYPE_EDIT_POLICY:
         return $object->getEditPolicy();
       case PhabricatorTransactions::TYPE_JOIN_POLICY:
         return $object->getJoinPolicy();
       case PhabricatorTransactions::TYPE_SPACE:
         $space_phid = $object->getSpacePHID();
         if ($space_phid === null) {
           if ($this->getIsNewObject()) {
             // In this case, just return `null` so we know this is the initial
             // transaction and it should be hidden.
             return null;
           }
 
           $default_space = PhabricatorSpacesNamespaceQuery::getDefaultSpace();
           if ($default_space) {
             $space_phid = $default_space->getPHID();
           }
         }
+
         return $space_phid;
       case PhabricatorTransactions::TYPE_EDGE:
         $edge_type = $xaction->getMetadataValue('edge:type');
         if (!$edge_type) {
           throw new Exception(
             pht(
               "Edge transaction has no '%s'!",
               'edge:type'));
         }
 
         $old_edges = array();
         if ($object->getPHID()) {
           $edge_src = $object->getPHID();
 
           $old_edges = id(new PhabricatorEdgeQuery())
             ->withSourcePHIDs(array($edge_src))
             ->withEdgeTypes(array($edge_type))
             ->needEdgeData(true)
             ->execute();
 
           $old_edges = $old_edges[$edge_src][$edge_type];
         }
         return $old_edges;
       case PhabricatorTransactions::TYPE_CUSTOMFIELD:
         // NOTE: Custom fields have their old value pre-populated when they are
         // built by PhabricatorCustomFieldList.
         return $xaction->getOldValue();
       case PhabricatorTransactions::TYPE_COMMENT:
         return null;
       default:
         return $this->getCustomTransactionOldValue($object, $xaction);
     }
   }
 
   private function getTransactionNewValue(
     PhabricatorLiskDAO $object,
     PhabricatorApplicationTransaction $xaction) {
     switch ($xaction->getTransactionType()) {
       case PhabricatorTransactions::TYPE_SUBSCRIBERS:
         return $this->getPHIDTransactionNewValue($xaction);
       case PhabricatorTransactions::TYPE_VIEW_POLICY:
       case PhabricatorTransactions::TYPE_EDIT_POLICY:
       case PhabricatorTransactions::TYPE_JOIN_POLICY:
       case PhabricatorTransactions::TYPE_BUILDABLE:
       case PhabricatorTransactions::TYPE_TOKEN:
       case PhabricatorTransactions::TYPE_INLINESTATE:
         return $xaction->getNewValue();
       case PhabricatorTransactions::TYPE_SPACE:
         $space_phid = $xaction->getNewValue();
         if (!strlen($space_phid)) {
-          // If an install has no Spaces, we might end up with the empty string
-          // here instead of a strict `null`. Just make this work like callers
-          // might reasonably expect.
-          return null;
+          // If an install has no Spaces or the Spaces controls are not visible
+          // to the viewer, we might end up with the empty string here instead
+          // of a strict `null`, because some controller just used `getStr()`
+          // to read the space PHID from the request.
+          // Just make this work like callers might reasonably expect so we
+          // don't need to handle this specially in every EditController.
+          return $this->getActor()->getDefaultSpacePHID();
         } else {
           return $space_phid;
         }
       case PhabricatorTransactions::TYPE_EDGE:
         return $this->getEdgeTransactionNewValue($xaction);
       case PhabricatorTransactions::TYPE_CUSTOMFIELD:
         $field = $this->getCustomFieldForTransaction($object, $xaction);
         return $field->getNewValueFromApplicationTransactions($xaction);
       case PhabricatorTransactions::TYPE_COMMENT:
         return null;
       default:
         return $this->getCustomTransactionNewValue($object, $xaction);
     }
   }
 
   protected function getCustomTransactionOldValue(
     PhabricatorLiskDAO $object,
     PhabricatorApplicationTransaction $xaction) {
     throw new Exception(pht('Capability not supported!'));
   }
 
   protected function getCustomTransactionNewValue(
     PhabricatorLiskDAO $object,
     PhabricatorApplicationTransaction $xaction) {
     throw new Exception(pht('Capability not supported!'));
   }
 
   protected function transactionHasEffect(
     PhabricatorLiskDAO $object,
     PhabricatorApplicationTransaction $xaction) {
 
     switch ($xaction->getTransactionType()) {
       case PhabricatorTransactions::TYPE_COMMENT:
         return $xaction->hasComment();
       case PhabricatorTransactions::TYPE_CUSTOMFIELD:
         $field = $this->getCustomFieldForTransaction($object, $xaction);
         return $field->getApplicationTransactionHasEffect($xaction);
       case PhabricatorTransactions::TYPE_EDGE:
         // A straight value comparison here doesn't always get the right
         // result, because newly added edges aren't fully populated. Instead,
         // compare the changes in a more granular way.
         $old = $xaction->getOldValue();
         $new = $xaction->getNewValue();
 
         $old_dst = array_keys($old);
         $new_dst = array_keys($new);
 
         // NOTE: For now, we don't consider edge reordering to be a change.
         // We have very few order-dependent edges and effectively no order
         // oriented UI. This might change in the future.
         sort($old_dst);
         sort($new_dst);
 
         if ($old_dst !== $new_dst) {
           // We've added or removed edges, so this transaction definitely
           // has an effect.
           return true;
         }
 
         // We haven't added or removed edges, but we might have changed
         // edge data.
         foreach ($old as $key => $old_value) {
           $new_value = $new[$key];
           if ($old_value['data'] !== $new_value['data']) {
             return true;
           }
         }
 
         return false;
     }
 
     return ($xaction->getOldValue() !== $xaction->getNewValue());
   }
 
   protected function shouldApplyInitialEffects(
     PhabricatorLiskDAO $object,
     array $xactions) {
     return false;
   }
 
   protected function applyInitialEffects(
     PhabricatorLiskDAO $object,
     array $xactions) {
     throw new PhutilMethodNotImplementedException();
   }
 
   private function applyInternalEffects(
     PhabricatorLiskDAO $object,
     PhabricatorApplicationTransaction $xaction) {
 
     switch ($xaction->getTransactionType()) {
       case PhabricatorTransactions::TYPE_CUSTOMFIELD:
         $field = $this->getCustomFieldForTransaction($object, $xaction);
         return $field->applyApplicationTransactionInternalEffects($xaction);
       case PhabricatorTransactions::TYPE_BUILDABLE:
       case PhabricatorTransactions::TYPE_TOKEN:
       case PhabricatorTransactions::TYPE_VIEW_POLICY:
       case PhabricatorTransactions::TYPE_EDIT_POLICY:
       case PhabricatorTransactions::TYPE_JOIN_POLICY:
       case PhabricatorTransactions::TYPE_SUBSCRIBERS:
       case PhabricatorTransactions::TYPE_INLINESTATE:
       case PhabricatorTransactions::TYPE_EDGE:
       case PhabricatorTransactions::TYPE_SPACE:
       case PhabricatorTransactions::TYPE_COMMENT:
         return $this->applyBuiltinInternalTransaction($object, $xaction);
     }
 
     return $this->applyCustomInternalTransaction($object, $xaction);
   }
 
   private function applyExternalEffects(
     PhabricatorLiskDAO $object,
     PhabricatorApplicationTransaction $xaction) {
     switch ($xaction->getTransactionType()) {
       case PhabricatorTransactions::TYPE_SUBSCRIBERS:
         $subeditor = id(new PhabricatorSubscriptionsEditor())
           ->setObject($object)
           ->setActor($this->requireActor());
 
         $old_map = array_fuse($xaction->getOldValue());
         $new_map = array_fuse($xaction->getNewValue());
 
         $subeditor->unsubscribe(
           array_keys(
             array_diff_key($old_map, $new_map)));
 
         $subeditor->subscribeExplicit(
           array_keys(
             array_diff_key($new_map, $old_map)));
 
         $subeditor->save();
 
         // for the rest of these edits, subscribers should include those just
         // added as well as those just removed.
         $subscribers = array_unique(array_merge(
           $this->subscribers,
           $xaction->getOldValue(),
           $xaction->getNewValue()));
         $this->subscribers = $subscribers;
         return $this->applyBuiltinExternalTransaction($object, $xaction);
 
       case PhabricatorTransactions::TYPE_CUSTOMFIELD:
         $field = $this->getCustomFieldForTransaction($object, $xaction);
         return $field->applyApplicationTransactionExternalEffects($xaction);
       case PhabricatorTransactions::TYPE_EDGE:
       case PhabricatorTransactions::TYPE_BUILDABLE:
       case PhabricatorTransactions::TYPE_TOKEN:
       case PhabricatorTransactions::TYPE_VIEW_POLICY:
       case PhabricatorTransactions::TYPE_EDIT_POLICY:
       case PhabricatorTransactions::TYPE_JOIN_POLICY:
       case PhabricatorTransactions::TYPE_INLINESTATE:
       case PhabricatorTransactions::TYPE_SPACE:
       case PhabricatorTransactions::TYPE_COMMENT:
         return $this->applyBuiltinExternalTransaction($object, $xaction);
     }
 
     return $this->applyCustomExternalTransaction($object, $xaction);
   }
 
   protected function applyCustomInternalTransaction(
     PhabricatorLiskDAO $object,
     PhabricatorApplicationTransaction $xaction) {
     $type = $xaction->getTransactionType();
     throw new Exception(
       pht(
         "Transaction type '%s' is missing an internal apply implementation!",
         $type));
   }
 
   protected function applyCustomExternalTransaction(
     PhabricatorLiskDAO $object,
     PhabricatorApplicationTransaction $xaction) {
     $type = $xaction->getTransactionType();
     throw new Exception(
       pht(
         "Transaction type '%s' is missing an external apply implementation!",
         $type));
   }
 
   /**
    * @{class:PhabricatorTransactions} provides many built-in transactions
    * which should not require much - if any - code in specific applications.
    *
    * This method is a hook for the exceedingly-rare cases where you may need
    * to do **additional** work for built-in transactions. Developers should
    * extend this method, making sure to return the parent implementation
    * regardless of handling any transactions.
    *
    * See also @{method:applyBuiltinExternalTransaction}.
    */
   protected function applyBuiltinInternalTransaction(
     PhabricatorLiskDAO $object,
     PhabricatorApplicationTransaction $xaction) {
 
     switch ($xaction->getTransactionType()) {
       case PhabricatorTransactions::TYPE_VIEW_POLICY:
         $object->setViewPolicy($xaction->getNewValue());
         break;
       case PhabricatorTransactions::TYPE_EDIT_POLICY:
         $object->setEditPolicy($xaction->getNewValue());
         break;
       case PhabricatorTransactions::TYPE_JOIN_POLICY:
         $object->setJoinPolicy($xaction->getNewValue());
         break;
       case PhabricatorTransactions::TYPE_SPACE:
         $object->setSpacePHID($xaction->getNewValue());
         break;
     }
   }
 
   /**
    * See @{method::applyBuiltinInternalTransaction}.
    */
   protected function applyBuiltinExternalTransaction(
     PhabricatorLiskDAO $object,
     PhabricatorApplicationTransaction $xaction) {
 
     switch ($xaction->getTransactionType()) {
       case PhabricatorTransactions::TYPE_EDGE:
         if ($this->getIsInverseEdgeEditor()) {
           // If we're writing an inverse edge transaction, don't actually
           // do anything. The initiating editor on the other side of the
           // transaction will take care of the edge writes.
           break;
         }
 
         $old = $xaction->getOldValue();
         $new = $xaction->getNewValue();
         $src = $object->getPHID();
         $const = $xaction->getMetadataValue('edge:type');
 
         $type = PhabricatorEdgeType::getByConstant($const);
         if ($type->shouldWriteInverseTransactions()) {
           $this->applyInverseEdgeTransactions(
             $object,
             $xaction,
             $type->getInverseEdgeConstant());
         }
 
         foreach ($new as $dst_phid => $edge) {
           $new[$dst_phid]['src'] = $src;
         }
 
         $editor = new PhabricatorEdgeEditor();
 
         foreach ($old as $dst_phid => $edge) {
           if (!empty($new[$dst_phid])) {
             if ($old[$dst_phid]['data'] === $new[$dst_phid]['data']) {
               continue;
             }
           }
           $editor->removeEdge($src, $const, $dst_phid);
         }
 
         foreach ($new as $dst_phid => $edge) {
           if (!empty($old[$dst_phid])) {
             if ($old[$dst_phid]['data'] === $new[$dst_phid]['data']) {
               continue;
             }
           }
 
           $data = array(
             'data' => $edge['data'],
           );
 
           $editor->addEdge($src, $const, $dst_phid, $data);
         }
 
         $editor->save();
         break;
     }
   }
 
   /**
    * Fill in a transaction's common values, like author and content source.
    */
   protected function populateTransaction(
     PhabricatorLiskDAO $object,
     PhabricatorApplicationTransaction $xaction) {
 
     $actor = $this->getActor();
 
     // TODO: This needs to be more sophisticated once we have meta-policies.
     $xaction->setViewPolicy(PhabricatorPolicies::POLICY_PUBLIC);
 
     if ($actor->isOmnipotent()) {
       $xaction->setEditPolicy(PhabricatorPolicies::POLICY_NOONE);
     } else {
       $xaction->setEditPolicy($this->getActingAsPHID());
     }
 
     $xaction->setAuthorPHID($this->getActingAsPHID());
     $xaction->setContentSource($this->getContentSource());
     $xaction->attachViewer($actor);
     $xaction->attachObject($object);
 
     if ($object->getPHID()) {
       $xaction->setObjectPHID($object->getPHID());
     }
 
     return $xaction;
   }
 
   protected function didApplyInternalEffects(
     PhabricatorLiskDAO $object,
     array $xactions) {
     return $xactions;
   }
 
   protected function applyFinalEffects(
     PhabricatorLiskDAO $object,
     array $xactions) {
     return $xactions;
   }
 
   public function setContentSource(PhabricatorContentSource $content_source) {
     $this->contentSource = $content_source;
     return $this;
   }
 
   public function setContentSourceFromRequest(AphrontRequest $request) {
     return $this->setContentSource(
       PhabricatorContentSource::newFromRequest($request));
   }
 
   public function setContentSourceFromConduitRequest(
     ConduitAPIRequest $request) {
 
     $content_source = PhabricatorContentSource::newForSource(
       PhabricatorContentSource::SOURCE_CONDUIT,
       array());
 
     return $this->setContentSource($content_source);
   }
 
   public function getContentSource() {
     return $this->contentSource;
   }
 
   final public function applyTransactions(
     PhabricatorLiskDAO $object,
     array $xactions) {
 
     $this->object = $object;
     $this->xactions = $xactions;
     $this->isNewObject = ($object->getPHID() === null);
 
     $this->validateEditParameters($object, $xactions);
 
     $actor = $this->requireActor();
 
     // NOTE: Some transaction expansion requires that the edited object be
     // attached.
     foreach ($xactions as $xaction) {
       $xaction->attachObject($object);
       $xaction->attachViewer($actor);
     }
 
     $xactions = $this->expandTransactions($object, $xactions);
     $xactions = $this->expandSupportTransactions($object, $xactions);
     $xactions = $this->combineTransactions($xactions);
 
     foreach ($xactions as $xaction) {
       $xaction = $this->populateTransaction($object, $xaction);
     }
 
     $is_preview = $this->getIsPreview();
     $read_locking = false;
     $transaction_open = false;
 
     if (!$is_preview) {
       $errors = array();
       $type_map = mgroup($xactions, 'getTransactionType');
       foreach ($this->getTransactionTypes() as $type) {
         $type_xactions = idx($type_map, $type, array());
         $errors[] = $this->validateTransaction($object, $type, $type_xactions);
       }
 
       $errors[] = $this->validateAllTransactions($object, $xactions);
       $errors = array_mergev($errors);
 
       $continue_on_missing = $this->getContinueOnMissingFields();
       foreach ($errors as $key => $error) {
         if ($continue_on_missing && $error->getIsMissingFieldError()) {
           unset($errors[$key]);
         }
       }
 
       if ($errors) {
         throw new PhabricatorApplicationTransactionValidationException($errors);
       }
 
       $file_phids = $this->extractFilePHIDs($object, $xactions);
 
       if ($object->getID()) {
         foreach ($xactions as $xaction) {
 
           // If any of the transactions require a read lock, hold one and
           // reload the object. We need to do this fairly early so that the
           // call to `adjustTransactionValues()` (which populates old values)
           // is based on the synchronized state of the object, which may differ
           // from the state when it was originally loaded.
 
           if ($this->shouldReadLock($object, $xaction)) {
             $object->openTransaction();
             $object->beginReadLocking();
             $transaction_open = true;
             $read_locking = true;
             $object->reload();
             break;
           }
         }
       }
 
       if ($this->shouldApplyInitialEffects($object, $xactions)) {
         if (!$transaction_open) {
           $object->openTransaction();
           $transaction_open = true;
         }
       }
     }
 
     if ($this->shouldApplyInitialEffects($object, $xactions)) {
       $this->applyInitialEffects($object, $xactions);
     }
 
     foreach ($xactions as $xaction) {
       $this->adjustTransactionValues($object, $xaction);
     }
 
     $xactions = $this->filterTransactions($object, $xactions);
 
     if (!$xactions) {
       if ($read_locking) {
         $object->endReadLocking();
         $read_locking = false;
       }
       if ($transaction_open) {
         $object->killTransaction();
         $transaction_open = false;
       }
       return array();
     }
 
     // Now that we've merged, filtered, and combined transactions, check for
     // required capabilities.
     foreach ($xactions as $xaction) {
       $this->requireCapabilities($object, $xaction);
     }
 
     $xactions = $this->sortTransactions($xactions);
 
     if ($is_preview) {
       $this->loadHandles($xactions);
       return $xactions;
     }
 
     $comment_editor = id(new PhabricatorApplicationTransactionCommentEditor())
       ->setActor($actor)
       ->setActingAsPHID($this->getActingAsPHID())
       ->setContentSource($this->getContentSource());
 
     if (!$transaction_open) {
       $object->openTransaction();
     }
 
       foreach ($xactions as $xaction) {
         $this->applyInternalEffects($object, $xaction);
       }
 
       $xactions = $this->didApplyInternalEffects($object, $xactions);
 
       try {
         $object->save();
       } catch (AphrontDuplicateKeyQueryException $ex) {
         $object->killTransaction();
+
+        // This callback has an opportunity to throw a better exception,
+        // so execution may end here.
+        $this->didCatchDuplicateKeyException($object, $xactions, $ex);
+
         throw $ex;
       }
 
       foreach ($xactions as $xaction) {
         $xaction->setObjectPHID($object->getPHID());
         if ($xaction->getComment()) {
           $xaction->setPHID($xaction->generatePHID());
           $comment_editor->applyEdit($xaction, $xaction->getComment());
         } else {
           $xaction->save();
         }
       }
 
       if ($file_phids) {
         $this->attachFiles($object, $file_phids);
       }
 
       foreach ($xactions as $xaction) {
         $this->applyExternalEffects($object, $xaction);
       }
 
       $xactions = $this->applyFinalEffects($object, $xactions);
 
       if ($read_locking) {
         $object->endReadLocking();
         $read_locking = false;
       }
 
     $object->saveTransaction();
 
     // Now that we've completely applied the core transaction set, try to apply
     // Herald rules. Herald rules are allowed to either take direct actions on
     // the database (like writing flags), or take indirect actions (like saving
     // some targets for CC when we generate mail a little later), or return
     // transactions which we'll apply normally using another Editor.
 
     // First, check if *this* is a sub-editor which is itself applying Herald
     // rules: if it is, stop working and return so we don't descend into
     // madness.
 
     // Otherwise, we're not a Herald editor, so process Herald rules (possibly
     // using a Herald editor to apply resulting transactions) and then send out
     // mail, notifications, and feed updates about everything.
 
     if ($this->getIsHeraldEditor()) {
       // We are the Herald editor, so stop work here and return the updated
       // transactions.
       return $xactions;
     } else if ($this->getIsInverseEdgeEditor()) {
       // If we're applying inverse edge transactions, don't trigger Herald.
       // From a product perspective, the current set of inverse edges (most
       // often, mentions) aren't things users would expect to trigger Herald.
       // From a technical perspective, objects loaded by the inverse editor may
       // not have enough data to execute rules. At least for now, just stop
       // Herald from executing when applying inverse edges.
     } else if ($this->shouldApplyHeraldRules($object, $xactions)) {
       // We are not the Herald editor, so try to apply Herald rules.
       $herald_xactions = $this->applyHeraldRules($object, $xactions);
 
       if ($herald_xactions) {
         $xscript_id = $this->getHeraldTranscript()->getID();
         foreach ($herald_xactions as $herald_xaction) {
           $herald_xaction->setMetadataValue('herald:transcriptID', $xscript_id);
         }
 
         // NOTE: We're acting as the omnipotent user because rules deal with
         // their own policy issues. We use a synthetic author PHID (the
         // Herald application) as the author of record, so that transactions
         // will render in a reasonable way ("Herald assigned this task ...").
         $herald_actor = PhabricatorUser::getOmnipotentUser();
         $herald_phid = id(new PhabricatorHeraldApplication())->getPHID();
 
         // TODO: It would be nice to give transactions a more specific source
         // which points at the rule which generated them. You can figure this
         // out from transcripts, but it would be cleaner if you didn't have to.
 
         $herald_source = PhabricatorContentSource::newForSource(
           PhabricatorContentSource::SOURCE_HERALD,
           array());
 
         $herald_editor = newv(get_class($this), array())
           ->setContinueOnNoEffect(true)
           ->setContinueOnMissingFields(true)
           ->setParentMessageID($this->getParentMessageID())
           ->setIsHeraldEditor(true)
           ->setActor($herald_actor)
           ->setActingAsPHID($herald_phid)
           ->setContentSource($herald_source);
 
         $herald_xactions = $herald_editor->applyTransactions(
           $object,
           $herald_xactions);
 
-        $adapter = $this->getHeraldAdapter();
-        $this->heraldEmailPHIDs = $adapter->getEmailPHIDs();
-        $this->heraldForcedEmailPHIDs = $adapter->getForcedEmailPHIDs();
-
         // Merge the new transactions into the transaction list: we want to
         // send email and publish feed stories about them, too.
         $xactions = array_merge($xactions, $herald_xactions);
       }
+
+      // If Herald did not generate transactions, we may still need to handle
+      // "Send an Email" rules.
+      $adapter = $this->getHeraldAdapter();
+      $this->heraldEmailPHIDs = $adapter->getEmailPHIDs();
+      $this->heraldForcedEmailPHIDs = $adapter->getForcedEmailPHIDs();
     }
 
     $this->didApplyTransactions($xactions);
 
     if ($object instanceof PhabricatorCustomFieldInterface) {
       // Maybe this makes more sense to move into the search index itself? For
       // now I'm putting it here since I think we might end up with things that
       // need it to be up to date once the next page loads, but if we don't go
       // there we we could move it into search once search moves to the daemons.
 
       // It now happens in the search indexer as well, but the search indexer is
       // always daemonized, so the logic above still potentially holds. We could
       // possibly get rid of this. The major motivation for putting it in the
       // indexer was to enable reindexing to work.
 
       $fields = PhabricatorCustomField::getObjectFields(
         $object,
         PhabricatorCustomField::ROLE_APPLICATIONSEARCH);
       $fields->readFieldsFromStorage($object);
       $fields->rebuildIndexes($object);
     }
 
     $herald_xscript = $this->getHeraldTranscript();
     if ($herald_xscript) {
       $herald_header = $herald_xscript->getXHeraldRulesHeader();
       $herald_header = HeraldTranscript::saveXHeraldRulesHeader(
         $object->getPHID(),
         $herald_header);
     } else {
       $herald_header = HeraldTranscript::loadXHeraldRulesHeader(
         $object->getPHID());
     }
     $this->heraldHeader = $herald_header;
 
     // We're going to compute some of the data we'll use to publish these
     // transactions here, before queueing a worker.
     //
     // Primarily, this is more correct: we want to publish the object as it
     // exists right now. The worker may not execute for some time, and we want
     // to use the current To/CC list, not respect any changes which may occur
     // between now and when the worker executes.
     //
     // As a secondary benefit, this tends to reduce the amount of state that
     // Editors need to pass into workers.
     $object = $this->willPublish($object, $xactions);
 
     if (!$this->getDisableEmail()) {
       if ($this->shouldSendMail($object, $xactions)) {
         $this->mailToPHIDs = $this->getMailTo($object);
         $this->mailCCPHIDs = $this->getMailCC($object);
       }
     }
 
     if ($this->shouldPublishFeedStory($object, $xactions)) {
       $this->feedRelatedPHIDs = $this->getFeedRelatedPHIDs($object, $xactions);
       $this->feedNotifyPHIDs = $this->getFeedNotifyPHIDs($object, $xactions);
     }
 
     PhabricatorWorker::scheduleTask(
       'PhabricatorApplicationTransactionPublishWorker',
       array(
         'objectPHID' => $object->getPHID(),
         'actorPHID' => $this->getActingAsPHID(),
         'xactionPHIDs' => mpull($xactions, 'getPHID'),
         'state' => $this->getWorkerState(),
       ),
       array(
         'objectPHID' => $object->getPHID(),
         'priority' => PhabricatorWorker::PRIORITY_ALERTS,
       ));
 
     return $xactions;
   }
 
+  protected function didCatchDuplicateKeyException(
+    PhabricatorLiskDAO $object,
+    array $xactions,
+    Exception $ex) {
+    return;
+  }
+
   public function publishTransactions(
     PhabricatorLiskDAO $object,
     array $xactions) {
 
     // Hook for edges or other properties that may need (re-)loading
     $object = $this->willPublish($object, $xactions);
 
     $mailed = array();
     if (!$this->getDisableEmail()) {
       if ($this->shouldSendMail($object, $xactions)) {
         $mailed = $this->sendMail($object, $xactions);
       }
     }
 
     if ($this->supportsSearch()) {
       id(new PhabricatorSearchIndexer())
         ->queueDocumentForIndexing(
           $object->getPHID(),
           $this->getSearchContextParameter($object, $xactions));
     }
 
     if ($this->shouldPublishFeedStory($object, $xactions)) {
       $this->publishFeedStory(
         $object,
         $xactions,
         $mailed);
     }
 
     return $xactions;
   }
 
   protected function didApplyTransactions(array $xactions) {
     // Hook for subclasses.
     return;
   }
 
 
   /**
    * Determine if the editor should hold a read lock on the object while
    * applying a transaction.
    *
    * If the editor does not hold a lock, two editors may read an object at the
    * same time, then apply their changes without any synchronization. For most
    * transactions, this does not matter much. However, it is important for some
    * transactions. For example, if an object has a transaction count on it, both
    * editors may read the object with `count = 23`, then independently update it
    * and save the object with `count = 24` twice. This will produce the wrong
    * state: the object really has 25 transactions, but the count is only 24.
    *
    * Generally, transactions fall into one of four buckets:
    *
    *   - Append operations: Actions like adding a comment to an object purely
    *     add information to its state, and do not depend on the current object
    *     state in any way. These transactions never need to hold locks.
    *   - Overwrite operations: Actions like changing the title or description
    *     of an object replace the current value with a new value, so the end
    *     state is consistent without a lock. We currently do not lock these
    *     transactions, although we may in the future.
    *   - Edge operations: Edge and subscription operations have internal
    *     synchronization which limits the damage race conditions can cause.
    *     We do not currently lock these transactions, although we may in the
    *     future.
    *   - Update operations: Actions like incrementing a count on an object.
    *     These operations generally should use locks, unless it is not
    *     important that the state remain consistent in the presence of races.
    *
    * @param   PhabricatorLiskDAO  Object being updated.
    * @param   PhabricatorApplicationTransaction Transaction being applied.
    * @return  bool                True to synchronize the edit with a lock.
    */
   protected function shouldReadLock(
     PhabricatorLiskDAO $object,
     PhabricatorApplicationTransaction $xaction) {
     return false;
   }
 
   private function loadHandles(array $xactions) {
     $phids = array();
     foreach ($xactions as $key => $xaction) {
       $phids[$key] = $xaction->getRequiredHandlePHIDs();
     }
     $handles = array();
     $merged = array_mergev($phids);
     if ($merged) {
       $handles = id(new PhabricatorHandleQuery())
         ->setViewer($this->requireActor())
         ->withPHIDs($merged)
         ->execute();
     }
     foreach ($xactions as $key => $xaction) {
       $xaction->setHandles(array_select_keys($handles, $phids[$key]));
     }
   }
 
   private function loadSubscribers(PhabricatorLiskDAO $object) {
     if ($object->getPHID() &&
         ($object instanceof PhabricatorSubscribableInterface)) {
       $subs = PhabricatorSubscribersQuery::loadSubscribersForPHID(
         $object->getPHID());
       $this->subscribers = array_fuse($subs);
     } else {
       $this->subscribers = array();
     }
   }
 
   private function validateEditParameters(
     PhabricatorLiskDAO $object,
     array $xactions) {
 
     if (!$this->getContentSource()) {
       throw new PhutilInvalidStateException('setContentSource');
     }
 
     // Do a bunch of sanity checks that the incoming transactions are fresh.
     // They should be unsaved and have only "transactionType" and "newValue"
     // set.
 
     $types = array_fill_keys($this->getTransactionTypes(), true);
 
     assert_instances_of($xactions, 'PhabricatorApplicationTransaction');
     foreach ($xactions as $xaction) {
       if ($xaction->getPHID() || $xaction->getID()) {
         throw new PhabricatorApplicationTransactionStructureException(
           $xaction,
           pht('You can not apply transactions which already have IDs/PHIDs!'));
       }
       if ($xaction->getObjectPHID()) {
         throw new PhabricatorApplicationTransactionStructureException(
           $xaction,
           pht(
             'You can not apply transactions which already have %s!',
             'objectPHIDs'));
       }
       if ($xaction->getAuthorPHID()) {
         throw new PhabricatorApplicationTransactionStructureException(
           $xaction,
           pht(
             'You can not apply transactions which already have %s!',
             'authorPHIDs'));
       }
       if ($xaction->getCommentPHID()) {
         throw new PhabricatorApplicationTransactionStructureException(
           $xaction,
           pht(
             'You can not apply transactions which already have %s!',
             'commentPHIDs'));
       }
       if ($xaction->getCommentVersion() !== 0) {
         throw new PhabricatorApplicationTransactionStructureException(
           $xaction,
           pht(
             'You can not apply transactions which already have '.
             'commentVersions!'));
       }
 
       $expect_value = !$xaction->shouldGenerateOldValue();
       $has_value = $xaction->hasOldValue();
 
       if ($expect_value && !$has_value) {
         throw new PhabricatorApplicationTransactionStructureException(
           $xaction,
           pht(
             'This transaction is supposed to have an %s set, but it does not!',
             'oldValue'));
       }
 
       if ($has_value && !$expect_value) {
         throw new PhabricatorApplicationTransactionStructureException(
           $xaction,
           pht(
             'This transaction should generate its %s automatically, '.
             'but has already had one set!',
             'oldValue'));
       }
 
       $type = $xaction->getTransactionType();
       if (empty($types[$type])) {
         throw new PhabricatorApplicationTransactionStructureException(
           $xaction,
           pht(
             'Transaction has type "%s", but that transaction type is not '.
             'supported by this editor (%s).',
             $type,
             get_class($this)));
       }
     }
   }
 
   protected function requireCapabilities(
     PhabricatorLiskDAO $object,
     PhabricatorApplicationTransaction $xaction) {
 
     if ($this->getIsNewObject()) {
       return;
     }
 
     $actor = $this->requireActor();
     switch ($xaction->getTransactionType()) {
       case PhabricatorTransactions::TYPE_COMMENT:
         PhabricatorPolicyFilter::requireCapability(
           $actor,
           $object,
           PhabricatorPolicyCapability::CAN_VIEW);
         break;
       case PhabricatorTransactions::TYPE_VIEW_POLICY:
       case PhabricatorTransactions::TYPE_EDIT_POLICY:
       case PhabricatorTransactions::TYPE_JOIN_POLICY:
       case PhabricatorTransactions::TYPE_SPACE:
         PhabricatorPolicyFilter::requireCapability(
           $actor,
           $object,
           PhabricatorPolicyCapability::CAN_EDIT);
         break;
     }
   }
 
   private function buildSubscribeTransaction(
     PhabricatorLiskDAO $object,
     array $xactions,
     array $blocks) {
 
     if (!($object instanceof PhabricatorSubscribableInterface)) {
       return null;
     }
 
     if ($this->shouldEnableMentions($object, $xactions)) {
       $texts = array_mergev($blocks);
       $phids = PhabricatorMarkupEngine::extractPHIDsFromMentions(
         $this->getActor(),
         $texts);
     } else {
       $phids = array();
     }
 
     $this->mentionedPHIDs = $phids;
 
     if ($object->getPHID()) {
       // Don't try to subscribe already-subscribed mentions: we want to generate
       // a dialog about an action having no effect if the user explicitly adds
       // existing CCs, but not if they merely mention existing subscribers.
       $phids = array_diff($phids, $this->subscribers);
     }
 
     if ($phids) {
       $users = id(new PhabricatorPeopleQuery())
         ->setViewer($this->getActor())
         ->withPHIDs($phids)
         ->execute();
       $users = mpull($users, null, 'getPHID');
 
       foreach ($phids as $key => $phid) {
         // Do not subscribe mentioned users
         // who do not have VIEW Permissions
         if ($object instanceof PhabricatorPolicyInterface
           && !PhabricatorPolicyFilter::hasCapability(
           $users[$phid],
           $object,
           PhabricatorPolicyCapability::CAN_VIEW)
         ) {
           unset($phids[$key]);
         } else {
           if ($object->isAutomaticallySubscribed($phid)) {
             unset($phids[$key]);
           }
         }
       }
       $phids = array_values($phids);
     }
     // No else here to properly return null should we unset all subscriber
     if (!$phids) {
       return null;
     }
 
     $xaction = newv(get_class(head($xactions)), array());
     $xaction->setTransactionType(PhabricatorTransactions::TYPE_SUBSCRIBERS);
     $xaction->setNewValue(array('+' => $phids));
 
     return $xaction;
   }
 
   protected function getRemarkupBlocksFromTransaction(
     PhabricatorApplicationTransaction $transaction) {
     return $transaction->getRemarkupBlocks();
   }
 
   protected function mergeTransactions(
     PhabricatorApplicationTransaction $u,
     PhabricatorApplicationTransaction $v) {
 
     $type = $u->getTransactionType();
 
     switch ($type) {
       case PhabricatorTransactions::TYPE_SUBSCRIBERS:
         return $this->mergePHIDOrEdgeTransactions($u, $v);
       case PhabricatorTransactions::TYPE_EDGE:
         $u_type = $u->getMetadataValue('edge:type');
         $v_type = $v->getMetadataValue('edge:type');
         if ($u_type == $v_type) {
           return $this->mergePHIDOrEdgeTransactions($u, $v);
         }
         return null;
     }
 
     // By default, do not merge the transactions.
     return null;
   }
 
   /**
    * Optionally expand transactions which imply other effects. For example,
    * resigning from a revision in Differential implies removing yourself as
    * a reviewer.
    */
   private function expandTransactions(
     PhabricatorLiskDAO $object,
     array $xactions) {
 
     $results = array();
     foreach ($xactions as $xaction) {
       foreach ($this->expandTransaction($object, $xaction) as $expanded) {
         $results[] = $expanded;
       }
     }
 
     return $results;
   }
 
   protected function expandTransaction(
     PhabricatorLiskDAO $object,
     PhabricatorApplicationTransaction $xaction) {
     return array($xaction);
   }
 
 
   public function getExpandedSupportTransactions(
     PhabricatorLiskDAO $object,
     PhabricatorApplicationTransaction $xaction) {
 
     $xactions = array($xaction);
     $xactions = $this->expandSupportTransactions(
       $object,
       $xactions);
 
     if (count($xactions) == 1) {
       return array();
     }
 
     foreach ($xactions as $index => $cxaction) {
       if ($cxaction === $xaction) {
         unset($xactions[$index]);
         break;
       }
     }
 
     return $xactions;
   }
 
   private function expandSupportTransactions(
     PhabricatorLiskDAO $object,
     array $xactions) {
     $this->loadSubscribers($object);
 
     $xactions = $this->applyImplicitCC($object, $xactions);
 
     $blocks = array();
     foreach ($xactions as $key => $xaction) {
       $blocks[$key] = $this->getRemarkupBlocksFromTransaction($xaction);
     }
 
     $subscribe_xaction = $this->buildSubscribeTransaction(
       $object,
       $xactions,
       $blocks);
     if ($subscribe_xaction) {
       $xactions[] = $subscribe_xaction;
     }
 
     // TODO: For now, this is just a placeholder.
     $engine = PhabricatorMarkupEngine::getEngine('extract');
     $engine->setConfig('viewer', $this->requireActor());
 
     $block_xactions = $this->expandRemarkupBlockTransactions(
       $object,
       $xactions,
       $blocks,
       $engine);
 
     foreach ($block_xactions as $xaction) {
       $xactions[] = $xaction;
     }
 
     return $xactions;
   }
 
   private function expandRemarkupBlockTransactions(
     PhabricatorLiskDAO $object,
     array $xactions,
     $blocks,
     PhutilMarkupEngine $engine) {
 
     $block_xactions = $this->expandCustomRemarkupBlockTransactions(
       $object,
       $xactions,
       $blocks,
       $engine);
 
     $mentioned_phids = array();
     if ($this->shouldEnableMentions($object, $xactions)) {
       foreach ($blocks as $key => $xaction_blocks) {
         foreach ($xaction_blocks as $block) {
           $engine->markupText($block);
           $mentioned_phids += $engine->getTextMetadata(
             PhabricatorObjectRemarkupRule::KEY_MENTIONED_OBJECTS,
             array());
         }
       }
     }
 
     if (!$mentioned_phids) {
       return $block_xactions;
     }
 
     $mentioned_objects = id(new PhabricatorObjectQuery())
       ->setViewer($this->getActor())
       ->withPHIDs($mentioned_phids)
       ->execute();
 
     $mentionable_phids = array();
     if ($this->shouldEnableMentions($object, $xactions)) {
       foreach ($mentioned_objects as $mentioned_object) {
         if ($mentioned_object instanceof PhabricatorMentionableInterface) {
           $mentioned_phid = $mentioned_object->getPHID();
           if (idx($this->getUnmentionablePHIDMap(), $mentioned_phid)) {
             continue;
           }
           // don't let objects mention themselves
           if ($object->getPHID() && $mentioned_phid == $object->getPHID()) {
             continue;
           }
           $mentionable_phids[$mentioned_phid] = $mentioned_phid;
         }
       }
     }
 
     if ($mentionable_phids) {
       $edge_type = PhabricatorObjectMentionsObjectEdgeType::EDGECONST;
       $block_xactions[] = newv(get_class(head($xactions)), array())
         ->setIgnoreOnNoEffect(true)
         ->setTransactionType(PhabricatorTransactions::TYPE_EDGE)
         ->setMetadataValue('edge:type', $edge_type)
         ->setNewValue(array('+' => $mentionable_phids));
     }
 
     return $block_xactions;
   }
 
   protected function expandCustomRemarkupBlockTransactions(
     PhabricatorLiskDAO $object,
     array $xactions,
     $blocks,
     PhutilMarkupEngine $engine) {
     return array();
   }
 
 
   /**
    * Attempt to combine similar transactions into a smaller number of total
    * transactions. For example, two transactions which edit the title of an
    * object can be merged into a single edit.
    */
   private function combineTransactions(array $xactions) {
     $stray_comments = array();
 
     $result = array();
     $types = array();
     foreach ($xactions as $key => $xaction) {
       $type = $xaction->getTransactionType();
       if (isset($types[$type])) {
         foreach ($types[$type] as $other_key) {
           $merged = $this->mergeTransactions($result[$other_key], $xaction);
           if ($merged) {
             $result[$other_key] = $merged;
 
             if ($xaction->getComment() &&
                 ($xaction->getComment() !== $merged->getComment())) {
               $stray_comments[] = $xaction->getComment();
             }
 
             if ($result[$other_key]->getComment() &&
                 ($result[$other_key]->getComment() !== $merged->getComment())) {
               $stray_comments[] = $result[$other_key]->getComment();
             }
 
             // Move on to the next transaction.
             continue 2;
           }
         }
       }
       $result[$key] = $xaction;
       $types[$type][] = $key;
     }
 
     // If we merged any comments away, restore them.
     foreach ($stray_comments as $comment) {
       $xaction = newv(get_class(head($result)), array());
       $xaction->setTransactionType(PhabricatorTransactions::TYPE_COMMENT);
       $xaction->setComment($comment);
       $result[] = $xaction;
     }
 
     return array_values($result);
   }
 
   protected function mergePHIDOrEdgeTransactions(
     PhabricatorApplicationTransaction $u,
     PhabricatorApplicationTransaction $v) {
 
     $result = $u->getNewValue();
     foreach ($v->getNewValue() as $key => $value) {
       if ($u->getTransactionType() == PhabricatorTransactions::TYPE_EDGE) {
         if (empty($result[$key])) {
           $result[$key] = $value;
         } else {
           // We're merging two lists of edge adds, sets, or removes. Merge
           // them by merging individual PHIDs within them.
           $merged = $result[$key];
 
           foreach ($value as $dst => $v_spec) {
             if (empty($merged[$dst])) {
               $merged[$dst] = $v_spec;
             } else {
               // Two transactions are trying to perform the same operation on
               // the same edge. Normalize the edge data and then merge it. This
               // allows transactions to specify how data merges execute in a
               // precise way.
 
               $u_spec = $merged[$dst];
 
               if (!is_array($u_spec)) {
                 $u_spec = array('dst' => $u_spec);
               }
               if (!is_array($v_spec)) {
                 $v_spec = array('dst' => $v_spec);
               }
 
               $ux_data = idx($u_spec, 'data', array());
               $vx_data = idx($v_spec, 'data', array());
 
               $merged_data = $this->mergeEdgeData(
                 $u->getMetadataValue('edge:type'),
                 $ux_data,
                 $vx_data);
 
               $u_spec['data'] = $merged_data;
               $merged[$dst] = $u_spec;
             }
           }
 
           $result[$key] = $merged;
         }
       } else {
         $result[$key] = array_merge($value, idx($result, $key, array()));
       }
     }
     $u->setNewValue($result);
 
     // When combining an "ignore" transaction with a normal transaction, make
     // sure we don't propagate the "ignore" flag.
     if (!$v->getIgnoreOnNoEffect()) {
       $u->setIgnoreOnNoEffect(false);
     }
 
     return $u;
   }
 
   protected function mergeEdgeData($type, array $u, array $v) {
     return $v + $u;
   }
 
   protected function getPHIDTransactionNewValue(
     PhabricatorApplicationTransaction $xaction,
     $old = null) {
 
     if ($old !== null) {
       $old = array_fuse($old);
     } else {
       $old = array_fuse($xaction->getOldValue());
     }
 
     $new = $xaction->getNewValue();
     $new_add = idx($new, '+', array());
     unset($new['+']);
     $new_rem = idx($new, '-', array());
     unset($new['-']);
     $new_set = idx($new, '=', null);
     if ($new_set !== null) {
       $new_set = array_fuse($new_set);
     }
     unset($new['=']);
 
     if ($new) {
       throw new Exception(
         pht(
           "Invalid '%s' value for PHID transaction. Value should contain only ".
           "keys '%s' (add PHIDs), '%' (remove PHIDs) and '%s' (set PHIDS).",
           'new',
           '+',
           '-',
           '='));
     }
 
     $result = array();
 
     foreach ($old as $phid) {
       if ($new_set !== null && empty($new_set[$phid])) {
         continue;
       }
       $result[$phid] = $phid;
     }
 
     if ($new_set !== null) {
       foreach ($new_set as $phid) {
         $result[$phid] = $phid;
       }
     }
 
     foreach ($new_add as $phid) {
       $result[$phid] = $phid;
     }
 
     foreach ($new_rem as $phid) {
       unset($result[$phid]);
     }
 
     return array_values($result);
   }
 
   protected function getEdgeTransactionNewValue(
     PhabricatorApplicationTransaction $xaction) {
 
     $new = $xaction->getNewValue();
     $new_add = idx($new, '+', array());
     unset($new['+']);
     $new_rem = idx($new, '-', array());
     unset($new['-']);
     $new_set = idx($new, '=', null);
     unset($new['=']);
 
     if ($new) {
       throw new Exception(
         pht(
           "Invalid '%s' value for Edge transaction. Value should contain only ".
           "keys '%s' (add edges), '%s' (remove edges) and '%s' (set edges).",
           'new',
           '+',
           '-',
           '='));
     }
 
     $old = $xaction->getOldValue();
 
     $lists = array($new_set, $new_add, $new_rem);
     foreach ($lists as $list) {
       $this->checkEdgeList($list);
     }
 
     $result = array();
     foreach ($old as $dst_phid => $edge) {
       if ($new_set !== null && empty($new_set[$dst_phid])) {
         continue;
       }
       $result[$dst_phid] = $this->normalizeEdgeTransactionValue(
         $xaction,
         $edge,
         $dst_phid);
     }
 
     if ($new_set !== null) {
       foreach ($new_set as $dst_phid => $edge) {
         $result[$dst_phid] = $this->normalizeEdgeTransactionValue(
           $xaction,
           $edge,
           $dst_phid);
       }
     }
 
     foreach ($new_add as $dst_phid => $edge) {
       $result[$dst_phid] = $this->normalizeEdgeTransactionValue(
         $xaction,
         $edge,
         $dst_phid);
     }
 
     foreach ($new_rem as $dst_phid => $edge) {
       unset($result[$dst_phid]);
     }
 
     return $result;
   }
 
   private function checkEdgeList($list) {
     if (!$list) {
       return;
     }
     foreach ($list as $key => $item) {
       if (phid_get_type($key) === PhabricatorPHIDConstants::PHID_TYPE_UNKNOWN) {
         throw new Exception(
           pht(
             "Edge transactions must have destination PHIDs as in edge ".
             "lists (found key '%s').",
             $key));
       }
       if (!is_array($item) && $item !== $key) {
         throw new Exception(
           pht(
             "Edge transactions must have PHIDs or edge specs as values ".
             "(found value '%s').",
             $item));
       }
     }
   }
 
   private function normalizeEdgeTransactionValue(
     PhabricatorApplicationTransaction $xaction,
     $edge,
     $dst_phid) {
 
     if (!is_array($edge)) {
       if ($edge != $dst_phid) {
         throw new Exception(
           pht(
             'Transaction edge data must either be the edge PHID or an edge '.
             'specification dictionary.'));
       }
       $edge = array();
     } else {
       foreach ($edge as $key => $value) {
         switch ($key) {
           case 'src':
           case 'dst':
           case 'type':
           case 'data':
           case 'dateCreated':
           case 'dateModified':
           case 'seq':
           case 'dataID':
             break;
           default:
             throw new Exception(
               pht(
                 'Transaction edge specification contains unexpected key "%s".',
                 $key));
         }
       }
     }
 
     $edge['dst'] = $dst_phid;
 
     $edge_type = $xaction->getMetadataValue('edge:type');
     if (empty($edge['type'])) {
       $edge['type'] = $edge_type;
     } else {
       if ($edge['type'] != $edge_type) {
         $this_type = $edge['type'];
         throw new Exception(
           pht(
             "Edge transaction includes edge of type '%s', but ".
             "transaction is of type '%s'. Each edge transaction ".
             "must alter edges of only one type.",
             $this_type,
             $edge_type));
       }
     }
 
     if (!isset($edge['data'])) {
       $edge['data'] = array();
     }
 
     return $edge;
   }
 
   protected function sortTransactions(array $xactions) {
     $head = array();
     $tail = array();
 
     // Move bare comments to the end, so the actions precede them.
     foreach ($xactions as $xaction) {
       $type = $xaction->getTransactionType();
       if ($type == PhabricatorTransactions::TYPE_COMMENT) {
         $tail[] = $xaction;
       } else {
         $head[] = $xaction;
       }
     }
 
     return array_values(array_merge($head, $tail));
   }
 
 
   protected function filterTransactions(
     PhabricatorLiskDAO $object,
     array $xactions) {
 
     $type_comment = PhabricatorTransactions::TYPE_COMMENT;
 
     $no_effect = array();
     $has_comment = false;
     $any_effect = false;
     foreach ($xactions as $key => $xaction) {
       if ($this->transactionHasEffect($object, $xaction)) {
         if ($xaction->getTransactionType() != $type_comment) {
           $any_effect = true;
         }
       } else if ($xaction->getIgnoreOnNoEffect()) {
         unset($xactions[$key]);
       } else {
         $no_effect[$key] = $xaction;
       }
       if ($xaction->hasComment()) {
         $has_comment = true;
       }
     }
 
     if (!$no_effect) {
       return $xactions;
     }
 
     if (!$this->getContinueOnNoEffect() && !$this->getIsPreview()) {
       throw new PhabricatorApplicationTransactionNoEffectException(
         $no_effect,
         $any_effect,
         $has_comment);
     }
 
     if (!$any_effect && !$has_comment) {
       // If we only have empty comment transactions, just drop them all.
       return array();
     }
 
     foreach ($no_effect as $key => $xaction) {
       if ($xaction->getComment()) {
         $xaction->setTransactionType($type_comment);
         $xaction->setOldValue(null);
         $xaction->setNewValue(null);
       } else {
         unset($xactions[$key]);
       }
     }
 
     return $xactions;
   }
 
 
   /**
    * Hook for validating transactions. This callback will be invoked for each
    * available transaction type, even if an edit does not apply any transactions
    * of that type. This allows you to raise exceptions when required fields are
    * missing, by detecting that the object has no field value and there is no
    * transaction which sets one.
    *
    * @param PhabricatorLiskDAO Object being edited.
    * @param string Transaction type to validate.
    * @param list<PhabricatorApplicationTransaction> Transactions of given type,
    *   which may be empty if the edit does not apply any transactions of the
    *   given type.
    * @return list<PhabricatorApplicationTransactionValidationError> List of
    *   validation errors.
    */
   protected function validateTransaction(
     PhabricatorLiskDAO $object,
     $type,
     array $xactions) {
 
     $errors = array();
     switch ($type) {
       case PhabricatorTransactions::TYPE_VIEW_POLICY:
         $errors[] = $this->validatePolicyTransaction(
           $object,
           $xactions,
           $type,
           PhabricatorPolicyCapability::CAN_VIEW);
         break;
       case PhabricatorTransactions::TYPE_EDIT_POLICY:
         $errors[] = $this->validatePolicyTransaction(
           $object,
           $xactions,
           $type,
           PhabricatorPolicyCapability::CAN_EDIT);
         break;
       case PhabricatorTransactions::TYPE_SPACE:
         $errors[] = $this->validateSpaceTransactions(
           $object,
           $xactions,
           $type);
         break;
       case PhabricatorTransactions::TYPE_CUSTOMFIELD:
         $groups = array();
         foreach ($xactions as $xaction) {
           $groups[$xaction->getMetadataValue('customfield:key')][] = $xaction;
         }
 
         $field_list = PhabricatorCustomField::getObjectFields(
           $object,
           PhabricatorCustomField::ROLE_EDIT);
         $field_list->setViewer($this->getActor());
 
         $role_xactions = PhabricatorCustomField::ROLE_APPLICATIONTRANSACTIONS;
         foreach ($field_list->getFields() as $field) {
           if (!$field->shouldEnableForRole($role_xactions)) {
             continue;
           }
           $errors[] = $field->validateApplicationTransactions(
             $this,
             $type,
             idx($groups, $field->getFieldKey(), array()));
         }
         break;
     }
 
     return array_mergev($errors);
   }
 
   private function validatePolicyTransaction(
     PhabricatorLiskDAO $object,
     array $xactions,
     $transaction_type,
     $capability) {
 
     $actor = $this->requireActor();
     $errors = array();
     // Note $this->xactions is necessary; $xactions is $this->xactions of
     // $transaction_type
     $policy_object = $this->adjustObjectForPolicyChecks(
       $object,
       $this->xactions);
 
     // Make sure the user isn't editing away their ability to $capability this
     // object.
     foreach ($xactions as $xaction) {
       try {
         PhabricatorPolicyFilter::requireCapabilityWithForcedPolicy(
           $actor,
           $policy_object,
           $capability,
           $xaction->getNewValue());
       } catch (PhabricatorPolicyException $ex) {
         $errors[] = new PhabricatorApplicationTransactionValidationError(
           $transaction_type,
           pht('Invalid'),
           pht(
             'You can not select this %s policy, because you would no longer '.
             'be able to %s the object.',
             $capability,
             $capability),
           $xaction);
       }
     }
 
     if ($this->getIsNewObject()) {
       if (!$xactions) {
         $has_capability = PhabricatorPolicyFilter::hasCapability(
           $actor,
           $policy_object,
           $capability);
         if (!$has_capability) {
           $errors[] = new PhabricatorApplicationTransactionValidationError(
             $transaction_type,
             pht('Invalid'),
             pht(
               'The selected %s policy excludes you. Choose a %s policy '.
               'which allows you to %s the object.',
               $capability,
               $capability,
               $capability));
         }
       }
     }
 
     return $errors;
   }
 
 
   private function validateSpaceTransactions(
     PhabricatorLiskDAO $object,
     array $xactions,
     $transaction_type) {
     $errors = array();
 
-    $all_spaces = PhabricatorSpacesNamespaceQuery::getAllSpaces();
-    $viewer_spaces = PhabricatorSpacesNamespaceQuery::getViewerSpaces(
-      $this->getActor());
+    $actor = $this->getActor();
+
+    $has_spaces = PhabricatorSpacesNamespaceQuery::getViewerSpacesExist($actor);
+    $actor_spaces = PhabricatorSpacesNamespaceQuery::getViewerSpaces($actor);
+    $active_spaces = PhabricatorSpacesNamespaceQuery::getViewerActiveSpaces(
+      $actor);
     foreach ($xactions as $xaction) {
       $space_phid = $xaction->getNewValue();
 
       if ($space_phid === null) {
-        if (!$all_spaces) {
+        if (!$has_spaces) {
           // The install doesn't have any spaces, so this is fine.
           continue;
         }
 
         // The install has some spaces, so every object needs to be put
         // in a valid space.
         $errors[] = new PhabricatorApplicationTransactionValidationError(
           $transaction_type,
           pht('Invalid'),
           pht('You must choose a space for this object.'),
           $xaction);
         continue;
       }
 
       // If the PHID isn't `null`, it needs to be a valid space that the
       // viewer can see.
-      if (empty($viewer_spaces[$space_phid])) {
+      if (empty($actor_spaces[$space_phid])) {
         $errors[] = new PhabricatorApplicationTransactionValidationError(
           $transaction_type,
           pht('Invalid'),
           pht(
             'You can not shift this object in the selected space, because '.
             'the space does not exist or you do not have access to it.'),
           $xaction);
+      } else if (empty($active_spaces[$space_phid])) {
+
+        // It's OK to edit objects in an archived space, so just move on if
+        // we aren't adjusting the value.
+        $old_space_phid = $this->getTransactionOldValue($object, $xaction);
+        if ($space_phid == $old_space_phid) {
+          continue;
+        }
+
+        $errors[] = new PhabricatorApplicationTransactionValidationError(
+          $transaction_type,
+          pht('Archived'),
+          pht(
+            'You can not shift this object into the selected space, because '.
+            'the space is archived. Objects can not be created inside (or '.
+            'moved into) archived spaces.'),
+          $xaction);
       }
     }
 
     return $errors;
   }
 
 
   protected function adjustObjectForPolicyChecks(
     PhabricatorLiskDAO $object,
     array $xactions) {
 
-    return clone $object;
+    $copy = clone $object;
+
+    foreach ($xactions as $xaction) {
+      switch ($xaction->getTransactionType()) {
+        case PhabricatorTransactions::TYPE_SUBSCRIBERS:
+          $clone_xaction = clone $xaction;
+          $clone_xaction->setOldValue(array_values($this->subscribers));
+          $clone_xaction->setNewValue(
+            $this->getPHIDTransactionNewValue(
+              $clone_xaction));
+
+          PhabricatorPolicyRule::passTransactionHintToRule(
+            $copy,
+            new PhabricatorSubscriptionsSubscribersPolicyRule(),
+            array_fuse($clone_xaction->getNewValue()));
+
+          break;
+        case PhabricatorTransactions::TYPE_SPACE:
+          $space_phid = $this->getTransactionNewValue($object, $xaction);
+          $copy->setSpacePHID($space_phid);
+          break;
+      }
+    }
+
+    return $copy;
   }
 
   protected function validateAllTransactions(
     PhabricatorLiskDAO $object,
     array $xactions) {
     return array();
   }
 
   /**
    * Check for a missing text field.
    *
    * A text field is missing if the object has no value and there are no
    * transactions which set a value, or if the transactions remove the value.
    * This method is intended to make implementing @{method:validateTransaction}
    * more convenient:
    *
    *   $missing = $this->validateIsEmptyTextField(
    *     $object->getName(),
    *     $xactions);
    *
    * This will return `true` if the net effect of the object and transactions
    * is an empty field.
    *
    * @param wild Current field value.
    * @param list<PhabricatorApplicationTransaction> Transactions editing the
    *          field.
    * @return bool True if the field will be an empty text field after edits.
    */
   protected function validateIsEmptyTextField($field_value, array $xactions) {
     if (strlen($field_value) && empty($xactions)) {
       return false;
     }
 
     if ($xactions && strlen(last($xactions)->getNewValue())) {
       return false;
     }
 
     return true;
   }
 
 
 /* -(  Implicit CCs  )------------------------------------------------------- */
 
 
   /**
    * When a user interacts with an object, we might want to add them to CC.
    */
   final public function applyImplicitCC(
     PhabricatorLiskDAO $object,
     array $xactions) {
 
     if (!($object instanceof PhabricatorSubscribableInterface)) {
       // If the object isn't subscribable, we can't CC them.
       return $xactions;
     }
 
     $actor_phid = $this->getActingAsPHID();
 
     $type_user = PhabricatorPeopleUserPHIDType::TYPECONST;
     if (phid_get_type($actor_phid) != $type_user) {
       // Transactions by application actors like Herald, Harbormaster and
       // Diffusion should not CC the applications.
       return $xactions;
     }
 
     if ($object->isAutomaticallySubscribed($actor_phid)) {
       // If they're auto-subscribed, don't CC them.
       return $xactions;
     }
 
     $should_cc = false;
     foreach ($xactions as $xaction) {
       if ($this->shouldImplyCC($object, $xaction)) {
         $should_cc = true;
         break;
       }
     }
 
     if (!$should_cc) {
       // Only some types of actions imply a CC (like adding a comment).
       return $xactions;
     }
 
     if ($object->getPHID()) {
       if (isset($this->subscribers[$actor_phid])) {
         // If the user is already subscribed, don't implicitly CC them.
         return $xactions;
       }
 
       $unsub = PhabricatorEdgeQuery::loadDestinationPHIDs(
         $object->getPHID(),
         PhabricatorObjectHasUnsubscriberEdgeType::EDGECONST);
       $unsub = array_fuse($unsub);
       if (isset($unsub[$actor_phid])) {
         // If the user has previously unsubscribed from this object explicitly,
         // don't implicitly CC them.
         return $xactions;
       }
     }
 
     $xaction = newv(get_class(head($xactions)), array());
     $xaction->setTransactionType(PhabricatorTransactions::TYPE_SUBSCRIBERS);
     $xaction->setNewValue(array('+' => array($actor_phid)));
 
     array_unshift($xactions, $xaction);
 
     return $xactions;
   }
 
   protected function shouldImplyCC(
     PhabricatorLiskDAO $object,
     PhabricatorApplicationTransaction $xaction) {
 
     return $xaction->isCommentTransaction();
   }
 
 
 /* -(  Sending Mail  )------------------------------------------------------- */
 
 
   /**
    * @task mail
    */
   protected function shouldSendMail(
     PhabricatorLiskDAO $object,
     array $xactions) {
     return false;
   }
 
 
   /**
    * @task mail
    */
   protected function sendMail(
     PhabricatorLiskDAO $object,
     array $xactions) {
 
     $email_to = $this->mailToPHIDs;
     $email_cc = $this->mailCCPHIDs;
     $email_cc = array_merge($email_cc, $this->heraldEmailPHIDs);
 
     $targets = $this->buildReplyHandler($object)
       ->getMailTargets($email_to, $email_cc);
 
     // Set this explicitly before we start swapping out the effective actor.
     $this->setActingAsPHID($this->getActingAsPHID());
 
 
     $mailed = array();
     foreach ($targets as $target) {
       $original_actor = $this->getActor();
 
       $viewer = $target->getViewer();
       $this->setActor($viewer);
       $locale = PhabricatorEnv::beginScopedLocale($viewer->getTranslation());
 
       $caught = null;
       $mail = null;
       try {
         // Reload handles for the new viewer.
         $this->loadHandles($xactions);
 
         $mail = $this->sendMailToTarget($object, $xactions, $target);
       } catch (Exception $ex) {
         $caught = $ex;
       }
 
       $this->setActor($original_actor);
       unset($locale);
 
       if ($caught) {
         throw $ex;
       }
 
       if ($mail) {
         foreach ($mail->buildRecipientList() as $phid) {
           $mailed[$phid] = true;
         }
       }
     }
 
     return array_keys($mailed);
   }
 
   private function sendMailToTarget(
     PhabricatorLiskDAO $object,
     array $xactions,
     PhabricatorMailTarget $target) {
 
     // Check if any of the transactions are visible for this viewer. If we
     // don't have any visible transactions, don't send the mail.
 
     $any_visible = false;
     foreach ($xactions as $xaction) {
       if (!$xaction->shouldHideForMail($xactions)) {
         $any_visible = true;
         break;
       }
     }
 
     if (!$any_visible) {
       return null;
     }
 
     $mail = $this->buildMailTemplate($object);
     $body = $this->buildMailBody($object, $xactions);
 
     $mail_tags = $this->getMailTags($object, $xactions);
     $action = $this->getMailAction($object, $xactions);
 
     if (PhabricatorEnv::getEnvConfig('metamta.email-preferences')) {
       $this->addEmailPreferenceSectionToMailBody(
         $body,
         $object,
         $xactions);
     }
 
     $mail
       ->setFrom($this->getActingAsPHID())
       ->setSubjectPrefix($this->getMailSubjectPrefix())
       ->setVarySubjectPrefix('['.$action.']')
       ->setThreadID($this->getMailThreadID($object), $this->getIsNewObject())
       ->setRelatedPHID($object->getPHID())
       ->setExcludeMailRecipientPHIDs($this->getExcludeMailRecipientPHIDs())
       ->setForceHeraldMailRecipientPHIDs($this->heraldForcedEmailPHIDs)
       ->setMailTags($mail_tags)
       ->setIsBulk(true)
       ->setBody($body->render())
       ->setHTMLBody($body->renderHTML());
 
     foreach ($body->getAttachments() as $attachment) {
       $mail->addAttachment($attachment);
     }
 
     if ($this->heraldHeader) {
       $mail->addHeader('X-Herald-Rules', $this->heraldHeader);
     }
 
     if ($object instanceof PhabricatorProjectInterface) {
       $this->addMailProjectMetadata($object, $mail);
     }
 
     if ($this->getParentMessageID()) {
       $mail->setParentMessageID($this->getParentMessageID());
     }
 
     return $target->sendMail($mail);
   }
 
   private function addMailProjectMetadata(
     PhabricatorLiskDAO $object,
     PhabricatorMetaMTAMail $template) {
 
     $project_phids = PhabricatorEdgeQuery::loadDestinationPHIDs(
       $object->getPHID(),
       PhabricatorProjectObjectHasProjectEdgeType::EDGECONST);
 
     if (!$project_phids) {
       return;
     }
 
     // TODO: This viewer isn't quite right. It would be slightly better to use
     // the mail recipient, but that's not very easy given the way rendering
     // works today.
 
     $handles = id(new PhabricatorHandleQuery())
       ->setViewer($this->requireActor())
       ->withPHIDs($project_phids)
       ->execute();
 
     $project_tags = array();
     foreach ($handles as $handle) {
       if (!$handle->isComplete()) {
         continue;
       }
       $project_tags[] = '<'.$handle->getObjectName().'>';
     }
 
     if (!$project_tags) {
       return;
     }
 
     $project_tags = implode(', ', $project_tags);
     $template->addHeader('X-Phabricator-Projects', $project_tags);
   }
 
 
   protected function getMailThreadID(PhabricatorLiskDAO $object) {
     return $object->getPHID();
   }
 
 
   /**
    * @task mail
    */
   protected function getStrongestAction(
     PhabricatorLiskDAO $object,
     array $xactions) {
     return last(msort($xactions, 'getActionStrength'));
   }
 
 
   /**
    * @task mail
    */
   protected function buildReplyHandler(PhabricatorLiskDAO $object) {
     throw new Exception(pht('Capability not supported.'));
   }
 
   /**
    * @task mail
    */
   protected function getMailSubjectPrefix() {
     throw new Exception(pht('Capability not supported.'));
   }
 
 
   /**
    * @task mail
    */
   protected function getMailTags(
     PhabricatorLiskDAO $object,
     array $xactions) {
     $tags = array();
 
     foreach ($xactions as $xaction) {
       $tags[] = $xaction->getMailTags();
     }
 
     return array_mergev($tags);
   }
 
   /**
    * @task mail
    */
   public function getMailTagsMap() {
     // TODO: We should move shared mail tags, like "comment", here.
     return array();
   }
 
 
   /**
    * @task mail
    */
   protected function getMailAction(
     PhabricatorLiskDAO $object,
     array $xactions) {
     return $this->getStrongestAction($object, $xactions)->getActionName();
   }
 
 
   /**
    * @task mail
    */
   protected function buildMailTemplate(PhabricatorLiskDAO $object) {
     throw new Exception(pht('Capability not supported.'));
   }
 
 
   /**
    * @task mail
    */
   protected function getMailTo(PhabricatorLiskDAO $object) {
     throw new Exception(pht('Capability not supported.'));
   }
 
 
   /**
    * @task mail
    */
   protected function getMailCC(PhabricatorLiskDAO $object) {
     $phids = array();
     $has_support = false;
 
     if ($object instanceof PhabricatorSubscribableInterface) {
       $phid = $object->getPHID();
       $phids[] = PhabricatorSubscribersQuery::loadSubscribersForPHID($phid);
       $has_support = true;
     }
 
     if ($object instanceof PhabricatorProjectInterface) {
       $project_phids = PhabricatorEdgeQuery::loadDestinationPHIDs(
         $object->getPHID(),
         PhabricatorProjectObjectHasProjectEdgeType::EDGECONST);
 
       if ($project_phids) {
         $watcher_type = PhabricatorObjectHasWatcherEdgeType::EDGECONST;
 
         $query = id(new PhabricatorEdgeQuery())
           ->withSourcePHIDs($project_phids)
           ->withEdgeTypes(array($watcher_type));
         $query->execute();
 
         $watcher_phids = $query->getDestinationPHIDs();
         if ($watcher_phids) {
           // We need to do a visibility check for all the watchers, as
           // watching a project is not a guarantee that you can see objects
           // associated with it.
           $users = id(new PhabricatorPeopleQuery())
             ->setViewer($this->requireActor())
             ->withPHIDs($watcher_phids)
             ->execute();
 
           $watchers = array();
           foreach ($users as $user) {
             $can_see = PhabricatorPolicyFilter::hasCapability(
               $user,
               $object,
               PhabricatorPolicyCapability::CAN_VIEW);
             if ($can_see) {
               $watchers[] = $user->getPHID();
             }
           }
           $phids[] = $watchers;
         }
       }
 
       $has_support = true;
     }
 
     if (!$has_support) {
       throw new Exception(pht('Capability not supported.'));
     }
 
     return array_mergev($phids);
   }
 
 
   /**
    * @task mail
    */
   protected function buildMailBody(
     PhabricatorLiskDAO $object,
     array $xactions) {
 
     $body = new PhabricatorMetaMTAMailBody();
     $body->setViewer($this->requireActor());
 
     $this->addHeadersAndCommentsToMailBody($body, $xactions);
     $this->addCustomFieldsToMailBody($body, $object, $xactions);
     return $body;
   }
 
 
   /**
    * @task mail
    */
   protected function addEmailPreferenceSectionToMailBody(
     PhabricatorMetaMTAMailBody $body,
     PhabricatorLiskDAO $object,
     array $xactions) {
 
     $href = PhabricatorEnv::getProductionURI(
       '/settings/panel/emailpreferences/');
     $body->addLinkSection(pht('EMAIL PREFERENCES'), $href);
   }
 
 
   /**
    * @task mail
    */
   protected function addHeadersAndCommentsToMailBody(
     PhabricatorMetaMTAMailBody $body,
     array $xactions) {
 
     $headers = array();
     $comments = array();
 
     foreach ($xactions as $xaction) {
       if ($xaction->shouldHideForMail($xactions)) {
         continue;
       }
 
       $header = $xaction->getTitleForMail();
       if ($header !== null) {
         $headers[] = $header;
       }
 
       $comment = $xaction->getBodyForMail();
       if ($comment !== null) {
         $comments[] = $comment;
       }
     }
     $body->addRawSection(implode("\n", $headers));
 
     foreach ($comments as $comment) {
       $body->addRemarkupSection($comment);
     }
   }
 
   /**
    * @task mail
    */
   protected function addCustomFieldsToMailBody(
     PhabricatorMetaMTAMailBody $body,
     PhabricatorLiskDAO $object,
     array $xactions) {
 
     if ($object instanceof PhabricatorCustomFieldInterface) {
       $field_list = PhabricatorCustomField::getObjectFields(
         $object,
         PhabricatorCustomField::ROLE_TRANSACTIONMAIL);
       $field_list->setViewer($this->getActor());
       $field_list->readFieldsFromStorage($object);
 
       foreach ($field_list->getFields() as $field) {
         $field->updateTransactionMailBody(
           $body,
           $this,
           $xactions);
       }
     }
   }
 
 
 
 /* -(  Publishing Feed Stories  )-------------------------------------------- */
 
 
   /**
    * @task feed
    */
   protected function shouldPublishFeedStory(
     PhabricatorLiskDAO $object,
     array $xactions) {
     return false;
   }
 
 
   /**
    * @task feed
    */
   protected function getFeedStoryType() {
     return 'PhabricatorApplicationTransactionFeedStory';
   }
 
 
   /**
    * @task feed
    */
   protected function getFeedRelatedPHIDs(
     PhabricatorLiskDAO $object,
     array $xactions) {
 
     $phids = array(
       $object->getPHID(),
       $this->getActingAsPHID(),
     );
 
     if ($object instanceof PhabricatorProjectInterface) {
       $project_phids = PhabricatorEdgeQuery::loadDestinationPHIDs(
         $object->getPHID(),
         PhabricatorProjectObjectHasProjectEdgeType::EDGECONST);
       foreach ($project_phids as $project_phid) {
         $phids[] = $project_phid;
       }
     }
 
     return $phids;
   }
 
 
   /**
    * @task feed
    */
   protected function getFeedNotifyPHIDs(
     PhabricatorLiskDAO $object,
     array $xactions) {
 
     return array_unique(array_merge(
       $this->getMailTo($object),
       $this->getMailCC($object)));
   }
 
 
   /**
    * @task feed
    */
   protected function getFeedStoryData(
     PhabricatorLiskDAO $object,
     array $xactions) {
 
     $xactions = msort($xactions, 'getActionStrength');
     $xactions = array_reverse($xactions);
 
     return array(
       'objectPHID'        => $object->getPHID(),
       'transactionPHIDs'  => mpull($xactions, 'getPHID'),
     );
   }
 
 
   /**
    * @task feed
    */
   protected function publishFeedStory(
     PhabricatorLiskDAO $object,
     array $xactions,
     array $mailed_phids) {
 
     $xactions = mfilter($xactions, 'shouldHideForFeed', true);
 
     if (!$xactions) {
       return;
     }
 
     $related_phids = $this->feedRelatedPHIDs;
     $subscribed_phids = $this->feedNotifyPHIDs;
 
     $story_type = $this->getFeedStoryType();
     $story_data = $this->getFeedStoryData($object, $xactions);
 
     id(new PhabricatorFeedStoryPublisher())
       ->setStoryType($story_type)
       ->setStoryData($story_data)
       ->setStoryTime(time())
       ->setStoryAuthorPHID($this->getActingAsPHID())
       ->setRelatedPHIDs($related_phids)
       ->setPrimaryObjectPHID($object->getPHID())
       ->setSubscribedPHIDs($subscribed_phids)
       ->setMailRecipientPHIDs($mailed_phids)
       ->setMailTags($this->getMailTags($object, $xactions))
       ->publish();
   }
 
 
 /* -(  Search Index  )------------------------------------------------------- */
 
 
   /**
    * @task search
    */
   protected function supportsSearch() {
     return false;
   }
 
   /**
    * @task search
    */
   protected function getSearchContextParameter(
     PhabricatorLiskDAO $object,
     array $xactions) {
     return null;
   }
 
 
 /* -(  Herald Integration )-------------------------------------------------- */
 
 
   protected function shouldApplyHeraldRules(
     PhabricatorLiskDAO $object,
     array $xactions) {
     return false;
   }
 
   protected function buildHeraldAdapter(
     PhabricatorLiskDAO $object,
     array $xactions) {
     throw new Exception(pht('No herald adapter specified.'));
   }
 
   private function setHeraldAdapter(HeraldAdapter $adapter) {
     $this->heraldAdapter = $adapter;
     return $this;
   }
 
   protected function getHeraldAdapter() {
     return $this->heraldAdapter;
   }
 
   private function setHeraldTranscript(HeraldTranscript $transcript) {
     $this->heraldTranscript = $transcript;
     return $this;
   }
 
   protected function getHeraldTranscript() {
     return $this->heraldTranscript;
   }
 
   private function applyHeraldRules(
     PhabricatorLiskDAO $object,
     array $xactions) {
 
     $adapter = $this->buildHeraldAdapter($object, $xactions);
     $adapter->setContentSource($this->getContentSource());
     $adapter->setIsNewObject($this->getIsNewObject());
     if ($this->getApplicationEmail()) {
       $adapter->setApplicationEmail($this->getApplicationEmail());
     }
     $xscript = HeraldEngine::loadAndApplyRules($adapter);
 
     $this->setHeraldAdapter($adapter);
     $this->setHeraldTranscript($xscript);
 
     return array_merge(
       $this->didApplyHeraldRules($object, $adapter, $xscript),
       $adapter->getQueuedTransactions());
   }
 
   protected function didApplyHeraldRules(
     PhabricatorLiskDAO $object,
     HeraldAdapter $adapter,
     HeraldTranscript $transcript) {
     return array();
   }
 
 
 /* -(  Custom Fields  )------------------------------------------------------ */
 
 
   /**
    * @task customfield
    */
   private function getCustomFieldForTransaction(
     PhabricatorLiskDAO $object,
     PhabricatorApplicationTransaction $xaction) {
 
     $field_key = $xaction->getMetadataValue('customfield:key');
     if (!$field_key) {
       throw new Exception(
         pht(
         "Custom field transaction has no '%s'!",
         'customfield:key'));
     }
 
     $field = PhabricatorCustomField::getObjectField(
       $object,
       PhabricatorCustomField::ROLE_APPLICATIONTRANSACTIONS,
       $field_key);
 
     if (!$field) {
       throw new Exception(
         pht(
           "Custom field transaction has invalid '%s'; field '%s' ".
           "is disabled or does not exist.",
           'customfield:key',
           $field_key));
     }
 
     if (!$field->shouldAppearInApplicationTransactions()) {
       throw new Exception(
         pht(
           "Custom field transaction '%s' does not implement ".
           "integration for %s.",
           $field_key,
           'ApplicationTransactions'));
     }
 
     $field->setViewer($this->getActor());
 
     return $field;
   }
 
 
 /* -(  Files  )-------------------------------------------------------------- */
 
 
   /**
    * Extract the PHIDs of any files which these transactions attach.
    *
    * @task files
    */
   private function extractFilePHIDs(
     PhabricatorLiskDAO $object,
     array $xactions) {
 
     $blocks = array();
     foreach ($xactions as $xaction) {
       $blocks[] = $this->getRemarkupBlocksFromTransaction($xaction);
     }
     $blocks = array_mergev($blocks);
 
     $phids = array();
     if ($blocks) {
       $phids[] = PhabricatorMarkupEngine::extractFilePHIDsFromEmbeddedFiles(
         $this->getActor(),
         $blocks);
     }
 
     foreach ($xactions as $xaction) {
       $phids[] = $this->extractFilePHIDsFromCustomTransaction(
         $object,
         $xaction);
     }
 
     $phids = array_unique(array_filter(array_mergev($phids)));
     if (!$phids) {
       return array();
     }
 
     // Only let a user attach files they can actually see, since this would
     // otherwise let you access any file by attaching it to an object you have
     // view permission on.
 
     $files = id(new PhabricatorFileQuery())
       ->setViewer($this->getActor())
       ->withPHIDs($phids)
       ->execute();
 
     return mpull($files, 'getPHID');
   }
 
   /**
    * @task files
    */
   protected function extractFilePHIDsFromCustomTransaction(
     PhabricatorLiskDAO $object,
     PhabricatorApplicationTransaction $xaction) {
     return array();
   }
 
 
   /**
    * @task files
    */
   private function attachFiles(
     PhabricatorLiskDAO $object,
     array $file_phids) {
 
     if (!$file_phids) {
       return;
     }
 
     $editor = new PhabricatorEdgeEditor();
 
     $src = $object->getPHID();
     $type = PhabricatorObjectHasFileEdgeType::EDGECONST;
     foreach ($file_phids as $dst) {
       $editor->addEdge($src, $type, $dst);
     }
 
     $editor->save();
   }
 
   private function applyInverseEdgeTransactions(
     PhabricatorLiskDAO $object,
     PhabricatorApplicationTransaction $xaction,
     $inverse_type) {
 
     $old = $xaction->getOldValue();
     $new = $xaction->getNewValue();
 
     $add = array_keys(array_diff_key($new, $old));
     $rem = array_keys(array_diff_key($old, $new));
 
     $add = array_fuse($add);
     $rem = array_fuse($rem);
     $all = $add + $rem;
 
     $nodes = id(new PhabricatorObjectQuery())
       ->setViewer($this->requireActor())
       ->withPHIDs($all)
       ->execute();
 
     foreach ($nodes as $node) {
       if (!($node instanceof PhabricatorApplicationTransactionInterface)) {
         continue;
       }
 
       if ($node instanceof PhabricatorUser) {
         // TODO: At least for now, don't record inverse edge transactions
         // for users (for example, "alincoln joined project X"): Feed fills
         // this role instead.
         continue;
       }
 
       $editor = $node->getApplicationTransactionEditor();
       $template = $node->getApplicationTransactionTemplate();
       $target = $node->getApplicationTransactionObject();
 
       if (isset($add[$node->getPHID()])) {
         $edge_edit_type = '+';
       } else {
         $edge_edit_type = '-';
       }
 
       $template
         ->setTransactionType($xaction->getTransactionType())
         ->setMetadataValue('edge:type', $inverse_type)
         ->setNewValue(
           array(
             $edge_edit_type => array($object->getPHID() => $object->getPHID()),
           ));
 
       $editor
         ->setContinueOnNoEffect(true)
         ->setContinueOnMissingFields(true)
         ->setParentMessageID($this->getParentMessageID())
         ->setIsInverseEdgeEditor(true)
         ->setActor($this->requireActor())
         ->setActingAsPHID($this->getActingAsPHID())
         ->setContentSource($this->getContentSource());
 
       $editor->applyTransactions($target, array($template));
     }
   }
 
 
 /* -(  Workers  )------------------------------------------------------------ */
 
 
   /**
    * Load any object state which is required to publish transactions.
    *
    * This hook is invoked in the main process before we compute data related
    * to publishing transactions (like email "To" and "CC" lists), and again in
    * the worker before publishing occurs.
    *
    * @return object Publishable object.
    * @task workers
    */
   protected function willPublish(PhabricatorLiskDAO $object, array $xactions) {
     return $object;
   }
 
 
   /**
    * Convert the editor state to a serializable dictionary which can be passed
    * to a worker.
    *
    * This data will be loaded with @{method:loadWorkerState} in the worker.
    *
    * @return dict<string, wild> Serializable editor state.
    * @task workers
    */
   final private function getWorkerState() {
     $state = array();
     foreach ($this->getAutomaticStateProperties() as $property) {
       $state[$property] = $this->$property;
     }
 
     $state += array(
       'excludeMailRecipientPHIDs' => $this->getExcludeMailRecipientPHIDs(),
       'custom' => $this->getCustomWorkerState(),
     );
 
     return $state;
   }
 
 
   /**
    * Hook; return custom properties which need to be passed to workers.
    *
    * @return dict<string, wild> Custom properties.
    * @task workers
    */
   protected function getCustomWorkerState() {
     return array();
   }
 
 
   /**
    * Load editor state using a dictionary emitted by @{method:getWorkerState}.
    *
    * This method is used to load state when running worker operations.
    *
    * @param dict<string, wild> Editor state, from @{method:getWorkerState}.
    * @return this
    * @task workers
    */
   final public function loadWorkerState(array $state) {
     foreach ($this->getAutomaticStateProperties() as $property) {
       $this->$property = idx($state, $property);
     }
 
     $exclude = idx($state, 'excludeMailRecipientPHIDs', array());
     $this->setExcludeMailRecipientPHIDs($exclude);
 
     $custom = idx($state, 'custom', array());
     $this->loadCustomWorkerState($custom);
 
     return $this;
   }
 
 
   /**
    * Hook; set custom properties on the editor from data emitted by
    * @{method:getCustomWorkerState}.
    *
    * @param dict<string, wild> Custom state,
    *   from @{method:getCustomWorkerState}.
    * @return this
    * @task workers
    */
   protected function loadCustomWorkerState(array $state) {
     return $this;
   }
 
 
   /**
    * Get a list of object properties which should be automatically sent to
    * workers in the state data.
    *
    * These properties will be automatically stored and loaded by the editor in
    * the worker.
    *
    * @return list<string> List of properties.
    * @task workers
    */
   private function getAutomaticStateProperties() {
     return array(
       'parentMessageID',
       'disableEmail',
       'isNewObject',
       'heraldEmailPHIDs',
       'heraldForcedEmailPHIDs',
       'heraldHeader',
       'mailToPHIDs',
       'mailCCPHIDs',
       'feedNotifyPHIDs',
       'feedRelatedPHIDs',
     );
   }
 
 }
diff --git a/src/applications/transactions/replyhandler/PhabricatorApplicationTransactionReplyHandler.php b/src/applications/transactions/replyhandler/PhabricatorApplicationTransactionReplyHandler.php
index 14e27edc7..82f3eca2c 100644
--- a/src/applications/transactions/replyhandler/PhabricatorApplicationTransactionReplyHandler.php
+++ b/src/applications/transactions/replyhandler/PhabricatorApplicationTransactionReplyHandler.php
@@ -1,161 +1,177 @@
 <?php
 
 abstract class PhabricatorApplicationTransactionReplyHandler
   extends PhabricatorMailReplyHandler {
 
   abstract public function getObjectPrefix();
 
   public function getPrivateReplyHandlerEmailAddress(
     PhabricatorUser $user) {
     return $this->getDefaultPrivateReplyHandlerEmailAddress(
       $user,
       $this->getObjectPrefix());
   }
 
   public function getPublicReplyHandlerEmailAddress() {
     return $this->getDefaultPublicReplyHandlerEmailAddress(
       $this->getObjectPrefix());
   }
 
   private function newEditor(PhabricatorMetaMTAReceivedMail $mail) {
     $content_source = PhabricatorContentSource::newForSource(
       PhabricatorContentSource::SOURCE_EMAIL,
       array(
         'id' => $mail->getID(),
       ));
 
     $editor = $this->getMailReceiver()
       ->getApplicationTransactionEditor()
       ->setActor($this->getActor())
       ->setContentSource($content_source)
       ->setContinueOnMissingFields(true)
       ->setParentMessageID($mail->getMessageID())
       ->setExcludeMailRecipientPHIDs($this->getExcludeMailRecipientPHIDs());
 
     if ($this->getApplicationEmail()) {
       $editor->setApplicationEmail($this->getApplicationEmail());
     }
 
     return $editor;
   }
 
   private function newTransaction() {
     return $this->getMailReceiver()->getApplicationTransactionTemplate();
   }
 
   protected function didReceiveMail(
     PhabricatorMetaMTAReceivedMail $mail,
     $body) {
     return array();
   }
 
   protected function shouldCreateCommentFromMailBody() {
     return (bool)$this->getMailReceiver()->getID();
   }
 
   final protected function receiveEmail(PhabricatorMetaMTAReceivedMail $mail) {
     $viewer = $this->getActor();
     $object = $this->getMailReceiver();
+    $app_email = $this->getApplicationEmail();
+
+    $is_new = !$object->getID();
+
+    // If this is a new object which implements the Spaces interface and was
+    // created by sending mail to an ApplicationEmail address, put the object
+    // in the same Space the address is in.
+    if ($is_new) {
+      if ($object instanceof PhabricatorSpacesInterface) {
+        if ($app_email) {
+          $space_phid = PhabricatorSpacesNamespaceQuery::getObjectSpacePHID(
+            $app_email);
+          $object->setSpacePHID($space_phid);
+        }
+      }
+    }
 
     $body_data = $mail->parseBody();
     $body = $body_data['body'];
     $body = $this->enhanceBodyWithAttachments($body, $mail->getAttachments());
 
     $xactions = $this->didReceiveMail($mail, $body);
 
     // If this object is subscribable, subscribe all the users who were
     // CC'd on the message.
     if ($object instanceof PhabricatorSubscribableInterface) {
       $subscriber_phids = $mail->loadCCPHIDs();
       if ($subscriber_phids) {
         $xactions[] = $this->newTransaction()
           ->setTransactionType(PhabricatorTransactions::TYPE_SUBSCRIBERS)
           ->setNewValue(
             array(
               '+' => array($viewer->getPHID()),
             ));
       }
     }
 
     $command_xactions = $this->processMailCommands(
       $mail,
       $body_data['commands']);
     foreach ($command_xactions as $xaction) {
       $xactions[] = $xaction;
     }
 
     if ($this->shouldCreateCommentFromMailBody()) {
       $comment = $this
         ->newTransaction()
         ->getApplicationTransactionCommentObject()
         ->setContent($body);
 
       $xactions[] = $this->newTransaction()
         ->setTransactionType(PhabricatorTransactions::TYPE_COMMENT)
         ->attachComment($comment);
     }
 
     $target = $object->getApplicationTransactionObject();
 
     $this->newEditor($mail)
       ->setContinueOnNoEffect(true)
       ->applyTransactions($target, $xactions);
   }
 
   private function processMailCommands(
     PhabricatorMetaMTAReceivedMail $mail,
     array $command_list) {
 
     $viewer = $this->getActor();
     $object = $this->getMailReceiver();
 
     $list = MetaMTAEmailTransactionCommand::getAllCommandsForObject($object);
     $map = MetaMTAEmailTransactionCommand::getCommandMap($list);
 
     $xactions = array();
     foreach ($command_list as $command_argv) {
       $command = head($command_argv);
       $argv = array_slice($command_argv, 1);
 
       $handler = idx($map, phutil_utf8_strtolower($command));
       if ($handler) {
         $results = $handler->buildTransactions(
           $viewer,
           $object,
           $mail,
           $command,
           $argv);
         foreach ($results as $result) {
           $xactions[] = $result;
         }
       } else {
         $valid_commands = array();
         foreach ($list as $valid_command) {
           $aliases = $valid_command->getCommandAliases();
           if ($aliases) {
             foreach ($aliases as $key => $alias) {
               $aliases[$key] = '!'.$alias;
             }
             $aliases = implode(', ', $aliases);
             $valid_commands[] = pht(
               '!%s (or %s)',
               $valid_command->getCommand(),
               $aliases);
           } else {
             $valid_commands[] = '!'.$valid_command->getCommand();
           }
         }
 
         throw new Exception(
           pht(
             'The command "!%s" is not a supported mail command. Valid '.
             'commands for this object are: %s.',
             $command,
             implode(', ', $valid_commands)));
       }
     }
 
     return $xactions;
   }
 
 }
diff --git a/src/applications/transactions/storage/PhabricatorApplicationTransaction.php b/src/applications/transactions/storage/PhabricatorApplicationTransaction.php
index 7439c8728..e8811da76 100644
--- a/src/applications/transactions/storage/PhabricatorApplicationTransaction.php
+++ b/src/applications/transactions/storage/PhabricatorApplicationTransaction.php
@@ -1,1272 +1,1272 @@
 <?php
 
 abstract class PhabricatorApplicationTransaction
   extends PhabricatorLiskDAO
   implements
     PhabricatorPolicyInterface,
     PhabricatorDestructibleInterface {
 
   const TARGET_TEXT = 'text';
   const TARGET_HTML = 'html';
 
   protected $phid;
   protected $objectPHID;
   protected $authorPHID;
   protected $viewPolicy;
   protected $editPolicy;
 
   protected $commentPHID;
   protected $commentVersion = 0;
   protected $transactionType;
   protected $oldValue;
   protected $newValue;
   protected $metadata = array();
 
   protected $contentSource;
 
   private $comment;
   private $commentNotLoaded;
 
   private $handles;
   private $renderingTarget = self::TARGET_HTML;
   private $transactionGroup = array();
   private $viewer = self::ATTACHABLE;
   private $object = self::ATTACHABLE;
   private $oldValueHasBeenSet = false;
 
   private $ignoreOnNoEffect;
 
 
   /**
    * Flag this transaction as a pure side-effect which should be ignored when
    * applying transactions if it has no effect, even if transaction application
    * would normally fail. This both provides users with better error messages
    * and allows transactions to perform optional side effects.
    */
   public function setIgnoreOnNoEffect($ignore) {
     $this->ignoreOnNoEffect = $ignore;
     return $this;
   }
 
   public function getIgnoreOnNoEffect() {
     return $this->ignoreOnNoEffect;
   }
 
   public function shouldGenerateOldValue() {
     switch ($this->getTransactionType()) {
       case PhabricatorTransactions::TYPE_BUILDABLE:
       case PhabricatorTransactions::TYPE_TOKEN:
       case PhabricatorTransactions::TYPE_CUSTOMFIELD:
       case PhabricatorTransactions::TYPE_INLINESTATE:
         return false;
     }
     return true;
   }
 
   abstract public function getApplicationTransactionType();
 
   private function getApplicationObjectTypeName() {
     $types = PhabricatorPHIDType::getAllTypes();
 
     $type = idx($types, $this->getApplicationTransactionType());
     if ($type) {
       return $type->getTypeName();
     }
 
     return pht('Object');
   }
 
   public function getApplicationTransactionCommentObject() {
     throw new PhutilMethodNotImplementedException();
   }
 
   public function getApplicationTransactionViewObject() {
     return new PhabricatorApplicationTransactionView();
   }
 
   public function getMetadataValue($key, $default = null) {
     return idx($this->metadata, $key, $default);
   }
 
   public function setMetadataValue($key, $value) {
     $this->metadata[$key] = $value;
     return $this;
   }
 
   public function generatePHID() {
     $type = PhabricatorApplicationTransactionTransactionPHIDType::TYPECONST;
     $subtype = $this->getApplicationTransactionType();
 
     return PhabricatorPHID::generateNewPHID($type, $subtype);
   }
 
   protected function getConfiguration() {
     return array(
       self::CONFIG_AUX_PHID => true,
       self::CONFIG_SERIALIZATION => array(
         'oldValue' => self::SERIALIZATION_JSON,
         'newValue' => self::SERIALIZATION_JSON,
         'metadata' => self::SERIALIZATION_JSON,
       ),
       self::CONFIG_COLUMN_SCHEMA => array(
         'commentPHID' => 'phid?',
         'commentVersion' => 'uint32',
         'contentSource' => 'text',
         'transactionType' => 'text32',
       ),
       self::CONFIG_KEY_SCHEMA => array(
         'key_object' => array(
           'columns' => array('objectPHID'),
         ),
       ),
     ) + parent::getConfiguration();
   }
 
   public function setContentSource(PhabricatorContentSource $content_source) {
     $this->contentSource = $content_source->serialize();
     return $this;
   }
 
   public function getContentSource() {
     return PhabricatorContentSource::newFromSerialized($this->contentSource);
   }
 
   public function hasComment() {
     return $this->getComment() && strlen($this->getComment()->getContent());
   }
 
   public function getComment() {
     if ($this->commentNotLoaded) {
       throw new Exception(pht('Comment for this transaction was not loaded.'));
     }
     return $this->comment;
   }
 
   public function attachComment(
     PhabricatorApplicationTransactionComment $comment) {
     $this->comment = $comment;
     $this->commentNotLoaded = false;
     return $this;
   }
 
   public function setCommentNotLoaded($not_loaded) {
     $this->commentNotLoaded = $not_loaded;
     return $this;
   }
 
   public function attachObject($object) {
     $this->object = $object;
     return $this;
   }
 
   public function getObject() {
     return $this->assertAttached($this->object);
   }
 
   public function getRemarkupBlocks() {
     $blocks = array();
 
     switch ($this->getTransactionType()) {
       case PhabricatorTransactions::TYPE_CUSTOMFIELD:
         $field = $this->getTransactionCustomField();
         if ($field) {
           $custom_blocks = $field->getApplicationTransactionRemarkupBlocks(
             $this);
           foreach ($custom_blocks as $custom_block) {
             $blocks[] = $custom_block;
           }
         }
         break;
     }
 
     if ($this->getComment()) {
       $blocks[] = $this->getComment()->getContent();
     }
 
     return $blocks;
   }
 
   public function setOldValue($value) {
     $this->oldValueHasBeenSet = true;
     $this->writeField('oldValue', $value);
     return $this;
   }
 
   public function hasOldValue() {
     return $this->oldValueHasBeenSet;
   }
 
 
 /* -(  Rendering  )---------------------------------------------------------- */
 
   public function setRenderingTarget($rendering_target) {
     $this->renderingTarget = $rendering_target;
     return $this;
   }
 
   public function getRenderingTarget() {
     return $this->renderingTarget;
   }
 
   public function attachViewer(PhabricatorUser $viewer) {
     $this->viewer = $viewer;
     return $this;
   }
 
   public function getViewer() {
     return $this->assertAttached($this->viewer);
   }
 
   public function getRequiredHandlePHIDs() {
     $phids = array();
 
     $old = $this->getOldValue();
     $new = $this->getNewValue();
 
     $phids[] = array($this->getAuthorPHID());
     switch ($this->getTransactionType()) {
       case PhabricatorTransactions::TYPE_CUSTOMFIELD:
         $field = $this->getTransactionCustomField();
         if ($field) {
           $phids[] = $field->getApplicationTransactionRequiredHandlePHIDs(
             $this);
         }
         break;
       case PhabricatorTransactions::TYPE_SUBSCRIBERS:
         $phids[] = $old;
         $phids[] = $new;
         break;
       case PhabricatorTransactions::TYPE_EDGE:
         $phids[] = ipull($old, 'dst');
         $phids[] = ipull($new, 'dst');
         break;
       case PhabricatorTransactions::TYPE_EDIT_POLICY:
       case PhabricatorTransactions::TYPE_VIEW_POLICY:
       case PhabricatorTransactions::TYPE_JOIN_POLICY:
-        if (!PhabricatorPolicyQuery::isGlobalPolicy($old)) {
+        if (!PhabricatorPolicyQuery::isSpecialPolicy($old)) {
           $phids[] = array($old);
         }
-        if (!PhabricatorPolicyQuery::isGlobalPolicy($new)) {
+        if (!PhabricatorPolicyQuery::isSpecialPolicy($new)) {
           $phids[] = array($new);
         }
         break;
       case PhabricatorTransactions::TYPE_SPACE:
         if ($old) {
           $phids[] = array($old);
         }
         if ($new) {
           $phids[] = array($new);
         }
         break;
       case PhabricatorTransactions::TYPE_TOKEN:
         break;
       case PhabricatorTransactions::TYPE_BUILDABLE:
         $phid = $this->getMetadataValue('harbormaster:buildablePHID');
         if ($phid) {
           $phids[] = array($phid);
         }
         break;
     }
 
     if ($this->getComment()) {
       $phids[] = array($this->getComment()->getAuthorPHID());
     }
 
     return array_mergev($phids);
   }
 
   public function setHandles(array $handles) {
     $this->handles = $handles;
     return $this;
   }
 
   public function getHandle($phid) {
     if (empty($this->handles[$phid])) {
       throw new Exception(
         pht(
           'Transaction ("%s", of type "%s") requires a handle ("%s") that it '.
           'did not load.',
           $this->getPHID(),
           $this->getTransactionType(),
           $phid));
     }
     return $this->handles[$phid];
   }
 
   public function getHandleIfExists($phid) {
     return idx($this->handles, $phid);
   }
 
   public function getHandles() {
     if ($this->handles === null) {
       throw new Exception(
         pht('Transaction requires handles and it did not load them.'));
     }
     return $this->handles;
   }
 
   public function renderHandleLink($phid) {
     if ($this->renderingTarget == self::TARGET_HTML) {
       return $this->getHandle($phid)->renderLink();
     } else {
       return $this->getHandle($phid)->getLinkName();
     }
   }
 
   public function renderHandleList(array $phids) {
     $links = array();
     foreach ($phids as $phid) {
       $links[] = $this->renderHandleLink($phid);
     }
     if ($this->renderingTarget == self::TARGET_HTML) {
       return phutil_implode_html(', ', $links);
     } else {
       return implode(', ', $links);
     }
   }
 
   private function renderSubscriberList(array $phids, $change_type) {
     if ($this->getRenderingTarget() == self::TARGET_TEXT) {
       return $this->renderHandleList($phids);
     } else {
       $handles = array_select_keys($this->getHandles(), $phids);
       return id(new SubscriptionListStringBuilder())
         ->setHandles($handles)
         ->setObjectPHID($this->getPHID())
         ->buildTransactionString($change_type);
     }
   }
 
   protected function renderPolicyName($phid, $state = 'old') {
     $policy = PhabricatorPolicy::newFromPolicyAndHandle(
       $phid,
       $this->getHandleIfExists($phid));
     if ($this->renderingTarget == self::TARGET_HTML) {
       switch ($policy->getType()) {
         case PhabricatorPolicyType::TYPE_CUSTOM:
           $policy->setHref('/transactions/'.$state.'/'.$this->getPHID().'/');
           $policy->setWorkflow(true);
           break;
         default:
           break;
       }
       $output = $policy->renderDescription();
     } else {
       $output = hsprintf('%s', $policy->getFullName());
     }
     return $output;
   }
 
   public function getIcon() {
     switch ($this->getTransactionType()) {
       case PhabricatorTransactions::TYPE_COMMENT:
         $comment = $this->getComment();
         if ($comment && $comment->getIsRemoved()) {
           return 'fa-eraser';
         }
         return 'fa-comment';
       case PhabricatorTransactions::TYPE_SUBSCRIBERS:
         return 'fa-envelope';
       case PhabricatorTransactions::TYPE_VIEW_POLICY:
       case PhabricatorTransactions::TYPE_EDIT_POLICY:
       case PhabricatorTransactions::TYPE_JOIN_POLICY:
         return 'fa-lock';
       case PhabricatorTransactions::TYPE_EDGE:
         return 'fa-link';
       case PhabricatorTransactions::TYPE_BUILDABLE:
         return 'fa-wrench';
       case PhabricatorTransactions::TYPE_TOKEN:
         return 'fa-trophy';
       case PhabricatorTransactions::TYPE_SPACE:
         return 'fa-th-large';
     }
 
     return 'fa-pencil';
   }
 
   public function getToken() {
     switch ($this->getTransactionType()) {
       case PhabricatorTransactions::TYPE_TOKEN:
         $old = $this->getOldValue();
         $new = $this->getNewValue();
         if ($new) {
           $icon = substr($new, 10);
         } else {
           $icon = substr($old, 10);
         }
         return array($icon, !$this->getNewValue());
     }
 
     return array(null, null);
   }
 
   public function getColor() {
     switch ($this->getTransactionType()) {
       case PhabricatorTransactions::TYPE_COMMENT;
         $comment = $this->getComment();
         if ($comment && $comment->getIsRemoved()) {
           return 'black';
         }
         break;
       case PhabricatorTransactions::TYPE_BUILDABLE:
         switch ($this->getNewValue()) {
           case HarbormasterBuildable::STATUS_PASSED:
             return 'green';
           case HarbormasterBuildable::STATUS_FAILED:
             return 'red';
         }
         break;
     }
     return null;
   }
 
   protected function getTransactionCustomField() {
     switch ($this->getTransactionType()) {
       case PhabricatorTransactions::TYPE_CUSTOMFIELD:
         $key = $this->getMetadataValue('customfield:key');
         if (!$key) {
           return null;
         }
 
         $field = PhabricatorCustomField::getObjectField(
           $this->getObject(),
           PhabricatorCustomField::ROLE_APPLICATIONTRANSACTIONS,
           $key);
         if (!$field) {
           return null;
         }
 
         $field->setViewer($this->getViewer());
         return $field;
     }
 
     return null;
   }
 
   public function shouldHide() {
     switch ($this->getTransactionType()) {
       case PhabricatorTransactions::TYPE_VIEW_POLICY:
       case PhabricatorTransactions::TYPE_EDIT_POLICY:
       case PhabricatorTransactions::TYPE_JOIN_POLICY:
       case PhabricatorTransactions::TYPE_SPACE:
         if ($this->getOldValue() === null) {
           return true;
         } else {
           return false;
         }
         break;
       case PhabricatorTransactions::TYPE_CUSTOMFIELD:
         $field = $this->getTransactionCustomField();
         if ($field) {
           return $field->shouldHideInApplicationTransactions($this);
         }
       case PhabricatorTransactions::TYPE_EDGE:
         $edge_type = $this->getMetadataValue('edge:type');
         switch ($edge_type) {
           case PhabricatorObjectMentionsObjectEdgeType::EDGECONST:
             return true;
             break;
           case PhabricatorObjectMentionedByObjectEdgeType::EDGECONST:
             $new = ipull($this->getNewValue(), 'dst');
             $old = ipull($this->getOldValue(), 'dst');
             $add = array_diff($new, $old);
             $add_value = reset($add);
             $add_handle = $this->getHandle($add_value);
             if ($add_handle->getPolicyFiltered()) {
               return true;
             }
             return false;
             break;
           default:
             break;
         }
         break;
     }
 
     return false;
   }
 
   public function shouldHideForMail(array $xactions) {
     switch ($this->getTransactionType()) {
       case PhabricatorTransactions::TYPE_TOKEN:
         return true;
       case PhabricatorTransactions::TYPE_BUILDABLE:
         switch ($this->getNewValue()) {
           case HarbormasterBuildable::STATUS_FAILED:
             // For now, only ever send mail when builds fail. We might let
             // you customize this later, but in most cases this is probably
             // completely uninteresting.
             return false;
         }
         return true;
      case PhabricatorTransactions::TYPE_EDGE:
         $edge_type = $this->getMetadataValue('edge:type');
         switch ($edge_type) {
           case PhabricatorObjectMentionsObjectEdgeType::EDGECONST:
           case PhabricatorObjectMentionedByObjectEdgeType::EDGECONST:
             return true;
             break;
           default:
             break;
         }
         break;
     }
 
     // If a transaction publishes an inline comment:
     //
     //   - Don't show it if there are other kinds of transactions. The
     //     rationale here is that application mail will make the presence
     //     of inline comments obvious enough by including them prominently
     //     in the body. We could change this in the future if the obviousness
     //     needs to be increased.
     //   - If there are only inline transactions, only show the first
     //     transaction. The rationale is that seeing multiple "added an inline
     //     comment" transactions is not useful.
 
     if ($this->isInlineCommentTransaction()) {
       foreach ($xactions as $xaction) {
         if (!$xaction->isInlineCommentTransaction()) {
           return true;
         }
       }
       return ($this !== head($xactions));
     }
 
     return $this->shouldHide();
   }
 
   public function shouldHideForFeed() {
     switch ($this->getTransactionType()) {
       case PhabricatorTransactions::TYPE_TOKEN:
         return true;
       case PhabricatorTransactions::TYPE_BUILDABLE:
         switch ($this->getNewValue()) {
           case HarbormasterBuildable::STATUS_FAILED:
             // For now, don't notify on build passes either. These are pretty
             // high volume and annoying, with very little present value. We
             // might want to turn them back on in the specific case of
             // build successes on the current document?
             return false;
         }
         return true;
      case PhabricatorTransactions::TYPE_EDGE:
         $edge_type = $this->getMetadataValue('edge:type');
         switch ($edge_type) {
           case PhabricatorObjectMentionsObjectEdgeType::EDGECONST:
           case PhabricatorObjectMentionedByObjectEdgeType::EDGECONST:
             return true;
             break;
           default:
             break;
         }
         break;
      case PhabricatorTransactions::TYPE_INLINESTATE:
        return true;
     }
 
     return $this->shouldHide();
   }
 
   public function getTitleForMail() {
     return id(clone $this)->setRenderingTarget('text')->getTitle();
   }
 
   public function getBodyForMail() {
     if ($this->isInlineCommentTransaction()) {
       // We don't return inline comment content as mail body content, because
       // applications need to contextualize it (by adding line numbers, for
       // example) in order for it to make sense.
       return null;
     }
 
     $comment = $this->getComment();
     if ($comment && strlen($comment->getContent())) {
       return $comment->getContent();
     }
 
     return null;
   }
 
   public function getNoEffectDescription() {
 
     switch ($this->getTransactionType()) {
       case PhabricatorTransactions::TYPE_COMMENT:
         return pht('You can not post an empty comment.');
       case PhabricatorTransactions::TYPE_VIEW_POLICY:
         return pht(
           'This %s already has that view policy.',
           $this->getApplicationObjectTypeName());
       case PhabricatorTransactions::TYPE_EDIT_POLICY:
         return pht(
           'This %s already has that edit policy.',
           $this->getApplicationObjectTypeName());
       case PhabricatorTransactions::TYPE_JOIN_POLICY:
         return pht(
           'This %s already has that join policy.',
           $this->getApplicationObjectTypeName());
       case PhabricatorTransactions::TYPE_SUBSCRIBERS:
         return pht(
           'All users are already subscribed to this %s.',
           $this->getApplicationObjectTypeName());
       case PhabricatorTransactions::TYPE_SPACE:
         return pht('This object is already in that space.');
       case PhabricatorTransactions::TYPE_EDGE:
         return pht('Edges already exist; transaction has no effect.');
     }
 
     return pht('Transaction has no effect.');
   }
 
   public function getTitle() {
     $author_phid = $this->getAuthorPHID();
 
     $old = $this->getOldValue();
     $new = $this->getNewValue();
 
     switch ($this->getTransactionType()) {
       case PhabricatorTransactions::TYPE_COMMENT:
         return pht(
           '%s added a comment.',
           $this->renderHandleLink($author_phid));
       case PhabricatorTransactions::TYPE_VIEW_POLICY:
         return pht(
           '%s changed the visibility of this %s from "%s" to "%s".',
           $this->renderHandleLink($author_phid),
           $this->getApplicationObjectTypeName(),
           $this->renderPolicyName($old, 'old'),
           $this->renderPolicyName($new, 'new'));
       case PhabricatorTransactions::TYPE_EDIT_POLICY:
         return pht(
           '%s changed the edit policy of this %s from "%s" to "%s".',
           $this->renderHandleLink($author_phid),
           $this->getApplicationObjectTypeName(),
           $this->renderPolicyName($old, 'old'),
           $this->renderPolicyName($new, 'new'));
       case PhabricatorTransactions::TYPE_JOIN_POLICY:
         return pht(
           '%s changed the join policy of this %s from "%s" to "%s".',
           $this->renderHandleLink($author_phid),
           $this->getApplicationObjectTypeName(),
           $this->renderPolicyName($old, 'old'),
           $this->renderPolicyName($new, 'new'));
       case PhabricatorTransactions::TYPE_SPACE:
         return pht(
           '%s shifted this object from the %s space to the %s space.',
           $this->renderHandleLink($author_phid),
           $this->renderHandleLink($old),
           $this->renderHandleLink($new));
       case PhabricatorTransactions::TYPE_SUBSCRIBERS:
         $add = array_diff($new, $old);
         $rem = array_diff($old, $new);
 
         if ($add && $rem) {
           return pht(
             '%s edited subscriber(s), added %d: %s; removed %d: %s.',
             $this->renderHandleLink($author_phid),
             count($add),
             $this->renderSubscriberList($add, 'add'),
             count($rem),
             $this->renderSubscriberList($rem, 'rem'));
         } else if ($add) {
           return pht(
             '%s added %d subscriber(s): %s.',
             $this->renderHandleLink($author_phid),
             count($add),
             $this->renderSubscriberList($add, 'add'));
         } else if ($rem) {
           return pht(
             '%s removed %d subscriber(s): %s.',
             $this->renderHandleLink($author_phid),
             count($rem),
             $this->renderSubscriberList($rem, 'rem'));
         } else {
           // This is used when rendering previews, before the user actually
           // selects any CCs.
           return pht(
             '%s updated subscribers...',
             $this->renderHandleLink($author_phid));
         }
         break;
       case PhabricatorTransactions::TYPE_EDGE:
         $new = ipull($new, 'dst');
         $old = ipull($old, 'dst');
         $add = array_diff($new, $old);
         $rem = array_diff($old, $new);
         $type = $this->getMetadata('edge:type');
         $type = head($type);
 
         $type_obj = PhabricatorEdgeType::getByConstant($type);
 
         if ($add && $rem) {
           return $type_obj->getTransactionEditString(
             $this->renderHandleLink($author_phid),
             new PhutilNumber(count($add) + count($rem)),
             new PhutilNumber(count($add)),
             $this->renderHandleList($add),
             new PhutilNumber(count($rem)),
             $this->renderHandleList($rem));
         } else if ($add) {
           return $type_obj->getTransactionAddString(
             $this->renderHandleLink($author_phid),
             new PhutilNumber(count($add)),
             $this->renderHandleList($add));
         } else if ($rem) {
           return $type_obj->getTransactionRemoveString(
             $this->renderHandleLink($author_phid),
             new PhutilNumber(count($rem)),
             $this->renderHandleList($rem));
         } else {
           return $type_obj->getTransactionPreviewString(
             $this->renderHandleLink($author_phid));
         }
 
       case PhabricatorTransactions::TYPE_CUSTOMFIELD:
         $field = $this->getTransactionCustomField();
         if ($field) {
           return $field->getApplicationTransactionTitle($this);
         } else {
           return pht(
             '%s edited a custom field.',
             $this->renderHandleLink($author_phid));
         }
 
       case PhabricatorTransactions::TYPE_TOKEN:
         if ($old && $new) {
           return pht(
             '%s updated a token.',
             $this->renderHandleLink($author_phid));
         } else if ($old) {
           return pht(
             '%s rescinded a token.',
             $this->renderHandleLink($author_phid));
         } else {
           return pht(
             '%s awarded a token.',
             $this->renderHandleLink($author_phid));
         }
 
       case PhabricatorTransactions::TYPE_BUILDABLE:
         switch ($this->getNewValue()) {
           case HarbormasterBuildable::STATUS_BUILDING:
             return pht(
               '%s started building %s.',
               $this->renderHandleLink($author_phid),
               $this->renderHandleLink(
                 $this->getMetadataValue('harbormaster:buildablePHID')));
           case HarbormasterBuildable::STATUS_PASSED:
             return pht(
               '%s completed building %s.',
               $this->renderHandleLink($author_phid),
               $this->renderHandleLink(
                 $this->getMetadataValue('harbormaster:buildablePHID')));
           case HarbormasterBuildable::STATUS_FAILED:
             return pht(
               '%s failed to build %s!',
               $this->renderHandleLink($author_phid),
               $this->renderHandleLink(
                 $this->getMetadataValue('harbormaster:buildablePHID')));
           default:
             return null;
         }
 
       case PhabricatorTransactions::TYPE_INLINESTATE:
         $done = 0;
         $undone = 0;
         foreach ($new as $phid => $state) {
           if ($state == PhabricatorInlineCommentInterface::STATE_DONE) {
             $done++;
           } else {
             $undone++;
           }
         }
         if ($done && $undone) {
           return pht(
             '%s marked %s inline comment(s) as done and %s inline comment(s) '.
             'as not done.',
             $this->renderHandleLink($author_phid),
             new PhutilNumber($done),
             new PhutilNumber($undone));
         } else if ($done) {
           return pht(
             '%s marked %s inline comment(s) as done.',
             $this->renderHandleLink($author_phid),
             new PhutilNumber($done));
         } else {
           return pht(
             '%s marked %s inline comment(s) as not done.',
             $this->renderHandleLink($author_phid),
             new PhutilNumber($undone));
         }
         break;
 
       default:
         return pht(
           '%s edited this %s.',
           $this->renderHandleLink($author_phid),
           $this->getApplicationObjectTypeName());
     }
   }
 
   public function getTitleForFeed() {
     $author_phid = $this->getAuthorPHID();
     $object_phid = $this->getObjectPHID();
 
     $old = $this->getOldValue();
     $new = $this->getNewValue();
 
     switch ($this->getTransactionType()) {
       case PhabricatorTransactions::TYPE_COMMENT:
         return pht(
           '%s added a comment to %s.',
           $this->renderHandleLink($author_phid),
           $this->renderHandleLink($object_phid));
       case PhabricatorTransactions::TYPE_VIEW_POLICY:
         return pht(
           '%s changed the visibility for %s.',
           $this->renderHandleLink($author_phid),
           $this->renderHandleLink($object_phid));
       case PhabricatorTransactions::TYPE_EDIT_POLICY:
         return pht(
           '%s changed the edit policy for %s.',
           $this->renderHandleLink($author_phid),
           $this->renderHandleLink($object_phid));
       case PhabricatorTransactions::TYPE_JOIN_POLICY:
         return pht(
           '%s changed the join policy for %s.',
           $this->renderHandleLink($author_phid),
           $this->renderHandleLink($object_phid));
       case PhabricatorTransactions::TYPE_SUBSCRIBERS:
         return pht(
           '%s updated subscribers of %s.',
           $this->renderHandleLink($author_phid),
           $this->renderHandleLink($object_phid));
       case PhabricatorTransactions::TYPE_SPACE:
         return pht(
           '%s shifted %s from the %s space to the %s space.',
           $this->renderHandleLink($author_phid),
           $this->renderHandleLink($object_phid),
           $this->renderHandleLink($old),
           $this->renderHandleLink($new));
       case PhabricatorTransactions::TYPE_EDGE:
         $new = ipull($new, 'dst');
         $old = ipull($old, 'dst');
         $add = array_diff($new, $old);
         $rem = array_diff($old, $new);
         $type = $this->getMetadata('edge:type');
         $type = head($type);
 
         $type_obj = PhabricatorEdgeType::getByConstant($type);
 
         if ($add && $rem) {
           return $type_obj->getFeedEditString(
             $this->renderHandleLink($author_phid),
             $this->renderHandleLink($object_phid),
             new PhutilNumber(count($add) + count($rem)),
             new PhutilNumber(count($add)),
             $this->renderHandleList($add),
             new PhutilNumber(count($rem)),
             $this->renderHandleList($rem));
         } else if ($add) {
           return $type_obj->getFeedAddString(
             $this->renderHandleLink($author_phid),
             $this->renderHandleLink($object_phid),
             new PhutilNumber(count($add)),
             $this->renderHandleList($add));
         } else if ($rem) {
           return $type_obj->getFeedRemoveString(
             $this->renderHandleLink($author_phid),
             $this->renderHandleLink($object_phid),
             new PhutilNumber(count($rem)),
             $this->renderHandleList($rem));
         } else {
           return pht(
             '%s edited edge metadata for %s.',
             $this->renderHandleLink($author_phid),
             $this->renderHandleLink($object_phid));
         }
 
       case PhabricatorTransactions::TYPE_CUSTOMFIELD:
         $field = $this->getTransactionCustomField();
         if ($field) {
           return $field->getApplicationTransactionTitleForFeed($this);
         } else {
           return pht(
             '%s edited a custom field on %s.',
             $this->renderHandleLink($author_phid),
             $this->renderHandleLink($object_phid));
         }
       case PhabricatorTransactions::TYPE_BUILDABLE:
         switch ($this->getNewValue()) {
           case HarbormasterBuildable::STATUS_BUILDING:
             return pht(
               '%s started building %s for %s.',
               $this->renderHandleLink($author_phid),
               $this->renderHandleLink(
                 $this->getMetadataValue('harbormaster:buildablePHID')),
               $this->renderHandleLink($object_phid));
           case HarbormasterBuildable::STATUS_PASSED:
             return pht(
               '%s completed building %s for %s.',
               $this->renderHandleLink($author_phid),
               $this->renderHandleLink(
                 $this->getMetadataValue('harbormaster:buildablePHID')),
               $this->renderHandleLink($object_phid));
           case HarbormasterBuildable::STATUS_FAILED:
             return pht(
               '%s failed to build %s for %s.',
               $this->renderHandleLink($author_phid),
               $this->renderHandleLink(
                 $this->getMetadataValue('harbormaster:buildablePHID')),
               $this->renderHandleLink($object_phid));
           default:
             return null;
         }
 
     }
 
     return $this->getTitle();
   }
 
   public function getMarkupFieldsForFeed(PhabricatorFeedStory $story) {
     $fields = array();
 
     switch ($this->getTransactionType()) {
       case PhabricatorTransactions::TYPE_COMMENT:
         $text = $this->getComment()->getContent();
         if (strlen($text)) {
           $fields[] = 'comment/'.$this->getID();
         }
         break;
     }
 
     return $fields;
   }
 
   public function getMarkupTextForFeed(PhabricatorFeedStory $story, $field) {
     switch ($this->getTransactionType()) {
       case PhabricatorTransactions::TYPE_COMMENT:
         $text = $this->getComment()->getContent();
         return PhabricatorMarkupEngine::summarize($text);
     }
 
     return null;
   }
 
   public function getBodyForFeed(PhabricatorFeedStory $story) {
     $old = $this->getOldValue();
     $new = $this->getNewValue();
 
     $body = null;
 
     switch ($this->getTransactionType()) {
       case PhabricatorTransactions::TYPE_COMMENT:
         $text = $this->getComment()->getContent();
         if (strlen($text)) {
           $body = $story->getMarkupFieldOutput('comment/'.$this->getID());
         }
         break;
     }
 
     return $body;
   }
 
   public function getActionStrength() {
     if ($this->isInlineCommentTransaction()) {
       return 0.25;
     }
 
     switch ($this->getTransactionType()) {
       case PhabricatorTransactions::TYPE_COMMENT:
         return 0.5;
       case PhabricatorTransactions::TYPE_SUBSCRIBERS:
         $old = $this->getOldValue();
         $new = $this->getNewValue();
 
         $add = array_diff($old, $new);
         $rem = array_diff($new, $old);
 
         // If this action is the actor subscribing or unsubscribing themselves,
         // it is less interesting. In particular, if someone makes a comment and
         // also implicitly subscribes themselves, we should treat the
         // transaction group as "comment", not "subscribe". In this specific
         // case (one affected user, and that affected user it the actor),
         // decrease the action strength.
 
         if ((count($add) + count($rem)) != 1) {
           // Not exactly one CC change.
           break;
         }
 
         $affected_phid = head(array_merge($add, $rem));
         if ($affected_phid != $this->getAuthorPHID()) {
           // Affected user is someone else.
           break;
         }
 
         // Make this weaker than TYPE_COMMENT.
         return 0.25;
     }
     return 1.0;
   }
 
   public function isCommentTransaction() {
     if ($this->hasComment()) {
       return true;
     }
 
     switch ($this->getTransactionType()) {
       case PhabricatorTransactions::TYPE_COMMENT:
         return true;
     }
 
     return false;
   }
 
   public function isInlineCommentTransaction() {
     return false;
   }
 
   public function getActionName() {
     switch ($this->getTransactionType()) {
       case PhabricatorTransactions::TYPE_COMMENT:
         return pht('Commented On');
       case PhabricatorTransactions::TYPE_VIEW_POLICY:
       case PhabricatorTransactions::TYPE_EDIT_POLICY:
       case PhabricatorTransactions::TYPE_JOIN_POLICY:
         return pht('Changed Policy');
       case PhabricatorTransactions::TYPE_SUBSCRIBERS:
         return pht('Changed Subscribers');
       case PhabricatorTransactions::TYPE_BUILDABLE:
         switch ($this->getNewValue()) {
           case HarbormasterBuildable::STATUS_PASSED:
             return pht('Build Passed');
           case HarbormasterBuildable::STATUS_FAILED:
             return pht('Build Failed');
           default:
             return pht('Build Status');
         }
       default:
         return pht('Updated');
     }
   }
 
   public function getMailTags() {
     return array();
   }
 
   public function hasChangeDetails() {
     switch ($this->getTransactionType()) {
       case PhabricatorTransactions::TYPE_CUSTOMFIELD:
         $field = $this->getTransactionCustomField();
         if ($field) {
           return $field->getApplicationTransactionHasChangeDetails($this);
         }
         break;
     }
     return false;
   }
 
   public function renderChangeDetails(PhabricatorUser $viewer) {
     switch ($this->getTransactionType()) {
       case PhabricatorTransactions::TYPE_CUSTOMFIELD:
         $field = $this->getTransactionCustomField();
         if ($field) {
           return $field->getApplicationTransactionChangeDetails($this, $viewer);
         }
         break;
     }
 
     return $this->renderTextCorpusChangeDetails(
       $viewer,
       $this->getOldValue(),
       $this->getNewValue());
   }
 
   public function renderTextCorpusChangeDetails(
     PhabricatorUser $viewer,
     $old,
     $new) {
 
     require_celerity_resource('differential-changeset-view-css');
 
     $view = id(new PhabricatorApplicationTransactionTextDiffDetailView())
       ->setUser($viewer)
       ->setOldText($old)
       ->setNewText($new);
 
     return $view->render();
   }
 
   public function attachTransactionGroup(array $group) {
     assert_instances_of($group, __CLASS__);
     $this->transactionGroup = $group;
     return $this;
   }
 
   public function getTransactionGroup() {
     return $this->transactionGroup;
   }
 
   /**
    * Should this transaction be visually grouped with an existing transaction
    * group?
    *
    * @param list<PhabricatorApplicationTransaction> List of transactions.
    * @return bool True to display in a group with the other transactions.
    */
   public function shouldDisplayGroupWith(array $group) {
     $this_source = null;
     if ($this->getContentSource()) {
       $this_source = $this->getContentSource()->getSource();
     }
 
     foreach ($group as $xaction) {
       // Don't group transactions by different authors.
       if ($xaction->getAuthorPHID() != $this->getAuthorPHID()) {
         return false;
       }
 
       // Don't group transactions for different objects.
       if ($xaction->getObjectPHID() != $this->getObjectPHID()) {
         return false;
       }
 
       // Don't group anything into a group which already has a comment.
       if ($xaction->isCommentTransaction()) {
         return false;
       }
 
       // Don't group transactions from different content sources.
       $other_source = null;
       if ($xaction->getContentSource()) {
         $other_source = $xaction->getContentSource()->getSource();
       }
 
       if ($other_source != $this_source) {
         return false;
       }
 
       // Don't group transactions which happened more than 2 minutes apart.
       $apart = abs($xaction->getDateCreated() - $this->getDateCreated());
       if ($apart > (60 * 2)) {
         return false;
       }
     }
 
     return true;
   }
 
   public function renderExtraInformationLink() {
     $herald_xscript_id = $this->getMetadataValue('herald:transcriptID');
 
     if ($herald_xscript_id) {
       return phutil_tag(
         'a',
         array(
           'href' => '/herald/transcript/'.$herald_xscript_id.'/',
         ),
         pht('View Herald Transcript'));
     }
 
     return null;
   }
 
   public function renderAsTextForDoorkeeper(
     DoorkeeperFeedStoryPublisher $publisher,
     PhabricatorFeedStory $story,
     array $xactions) {
 
     $text = array();
     $body = array();
 
     foreach ($xactions as $xaction) {
       $xaction_body = $xaction->getBodyForMail();
       if ($xaction_body !== null) {
         $body[] = $xaction_body;
       }
 
       if ($xaction->shouldHideForMail($xactions)) {
         continue;
       }
 
       $old_target = $xaction->getRenderingTarget();
       $new_target = self::TARGET_TEXT;
       $xaction->setRenderingTarget($new_target);
 
       if ($publisher->getRenderWithImpliedContext()) {
         $text[] = $xaction->getTitle();
       } else {
         $text[] = $xaction->getTitleForFeed();
       }
 
       $xaction->setRenderingTarget($old_target);
     }
 
     $text = implode("\n", $text);
     $body = implode("\n\n", $body);
 
     return rtrim($text."\n\n".$body);
   }
 
 
 
 /* -(  PhabricatorPolicyInterface Implementation  )-------------------------- */
 
 
   public function getCapabilities() {
     return array(
       PhabricatorPolicyCapability::CAN_VIEW,
       PhabricatorPolicyCapability::CAN_EDIT,
     );
   }
 
   public function getPolicy($capability) {
     switch ($capability) {
       case PhabricatorPolicyCapability::CAN_VIEW:
         return $this->getViewPolicy();
       case PhabricatorPolicyCapability::CAN_EDIT:
         return $this->getEditPolicy();
     }
   }
 
   public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
     return ($viewer->getPHID() == $this->getAuthorPHID());
   }
 
   public function describeAutomaticCapability($capability) {
     return pht(
       'Transactions are visible to users that can see the object which was '.
       'acted upon. Some transactions - in particular, comments - are '.
       'editable by the transaction author.');
   }
 
 
 /* -(  PhabricatorDestructibleInterface  )----------------------------------- */
 
 
   public function destroyObjectPermanently(
     PhabricatorDestructionEngine $engine) {
 
     $this->openTransaction();
       $comment_template = null;
       try {
         $comment_template = $this->getApplicationTransactionCommentObject();
       } catch (Exception $ex) {
         // Continue; no comments for these transactions.
       }
 
       if ($comment_template) {
         $comments = $comment_template->loadAllWhere(
           'transactionPHID = %s',
           $this->getPHID());
         foreach ($comments as $comment) {
           $engine->destroyObject($comment);
         }
       }
 
       $this->delete();
     $this->saveTransaction();
   }
 
 
 }
diff --git a/src/applications/typeahead/storage/PhabricatorTypeaheadResult.php b/src/applications/typeahead/storage/PhabricatorTypeaheadResult.php
index 516617093..6ef913fc5 100644
--- a/src/applications/typeahead/storage/PhabricatorTypeaheadResult.php
+++ b/src/applications/typeahead/storage/PhabricatorTypeaheadResult.php
@@ -1,180 +1,180 @@
 <?php
 
-final class PhabricatorTypeaheadResult {
+final class PhabricatorTypeaheadResult extends Phobject {
 
   private $name;
   private $uri;
   private $phid;
   private $priorityString;
   private $displayName;
   private $displayType;
   private $imageURI;
   private $priorityType;
   private $imageSprite;
   private $icon;
   private $color;
   private $closed;
   private $tokenType;
   private $unique;
 
   public function setIcon($icon) {
     $this->icon = $icon;
     return $this;
   }
 
   public function setName($name) {
     $this->name = $name;
     return $this;
   }
 
   public function setURI($uri) {
     $this->uri = $uri;
     return $this;
   }
 
   public function setPHID($phid) {
     $this->phid = $phid;
     return $this;
   }
 
   public function setPriorityString($priority_string) {
     $this->priorityString = $priority_string;
     return $this;
   }
 
   public function setDisplayName($display_name) {
     $this->displayName = $display_name;
     return $this;
   }
 
   public function setDisplayType($display_type) {
     $this->displayType = $display_type;
     return $this;
   }
 
   public function setImageURI($image_uri) {
     $this->imageURI = $image_uri;
     return $this;
   }
 
   public function setPriorityType($priority_type) {
     $this->priorityType = $priority_type;
     return $this;
   }
 
   public function setImageSprite($image_sprite) {
     $this->imageSprite = $image_sprite;
     return $this;
   }
 
   public function setClosed($closed) {
     $this->closed = $closed;
     return $this;
   }
 
   public function getName() {
     return $this->name;
   }
 
   public function getDisplayName() {
     return coalesce($this->displayName, $this->getName());
   }
 
   public function getIcon() {
     return nonempty($this->icon, $this->getDefaultIcon());
   }
 
   public function getPHID() {
     return $this->phid;
   }
 
   public function setUnique($unique) {
     $this->unique = $unique;
     return $this;
   }
 
   public function setTokenType($type) {
     $this->tokenType = $type;
     return $this;
   }
 
   public function getTokenType() {
     if ($this->closed && !$this->tokenType) {
       return PhabricatorTypeaheadTokenView::TYPE_DISABLED;
     }
     return $this->tokenType;
   }
 
   public function setColor($color) {
     $this->color = $color;
     return $this;
   }
 
   public function getColor() {
     return $this->color;
   }
 
   public function getSortKey() {
     // Put unique results (special parameter functions) ahead of other
     // results.
     if ($this->unique) {
       $prefix = 'A';
     } else {
       $prefix = 'B';
     }
 
     return $prefix.phutil_utf8_strtolower($this->getName());
   }
 
   public function getWireFormat() {
     $data = array(
       $this->name,
       $this->uri ? (string)$this->uri : null,
       $this->phid,
       $this->priorityString,
       $this->displayName,
       $this->displayType,
       $this->imageURI ? (string)$this->imageURI : null,
       $this->priorityType,
       $this->getIcon(),
       $this->closed,
       $this->imageSprite ? (string)$this->imageSprite : null,
       $this->color,
       $this->tokenType,
       $this->unique ? 1 : null,
     );
     while (end($data) === null) {
       array_pop($data);
     }
     return $data;
   }
 
   /**
    * If the datasource did not specify an icon explicitly, try to select a
    * default based on PHID type.
    */
   private function getDefaultIcon() {
     static $icon_map;
     if ($icon_map === null) {
       $types = PhabricatorPHIDType::getAllTypes();
 
       $map = array();
       foreach ($types as $type) {
         $icon = $type->getTypeIcon();
         if ($icon !== null) {
           $map[$type->getTypeConstant()] = $icon;
         }
       }
 
       $icon_map = $map;
     }
 
     $phid_type = phid_get_type($this->phid);
     if (isset($icon_map[$phid_type])) {
       return $icon_map[$phid_type];
     }
 
     return null;
   }
 
 }
diff --git a/src/applications/uiexample/examples/PhabricatorUIExample.php b/src/applications/uiexample/examples/PhabricatorUIExample.php
index 2f2ffe0b5..5326cba3f 100644
--- a/src/applications/uiexample/examples/PhabricatorUIExample.php
+++ b/src/applications/uiexample/examples/PhabricatorUIExample.php
@@ -1,46 +1,46 @@
 <?php
 
-abstract class PhabricatorUIExample {
+abstract class PhabricatorUIExample extends Phobject {
 
   private $request;
 
   public function setRequest($request) {
     $this->request = $request;
     return $this;
   }
 
   public function getRequest() {
     return $this->request;
   }
 
   abstract public function getName();
   abstract public function getDescription();
   abstract public function renderExample();
 
   protected function createBasicDummyHandle($name, $type, $fullname = null,
     $uri = null) {
 
     $id = mt_rand(15, 9999);
     $handle = new PhabricatorObjectHandle();
     $handle->setName($name);
     $handle->setType($type);
     $handle->setPHID(PhabricatorPHID::generateNewPHID($type));
 
     if ($fullname) {
       $handle->setFullName($fullname);
     } else {
       $handle->setFullName(
         sprintf('%s%d: %s',
           substr($type, 0, 1),
           $id,
           $name));
     }
 
     if ($uri) {
       $handle->setURI($uri);
     }
 
     return $handle;
   }
 
 }
diff --git a/src/applications/xhprof/controller/PhabricatorXHProfSampleListController.php b/src/applications/xhprof/controller/PhabricatorXHProfSampleListController.php
index ad88695eb..3e2de9a54 100644
--- a/src/applications/xhprof/controller/PhabricatorXHProfSampleListController.php
+++ b/src/applications/xhprof/controller/PhabricatorXHProfSampleListController.php
@@ -1,95 +1,95 @@
 <?php
 
 final class PhabricatorXHProfSampleListController
   extends PhabricatorXHProfController {
 
   private $view;
 
   public function willProcessRequest(array $data) {
     $this->view = idx($data, 'view', 'all');
   }
 
   public function processRequest() {
     $request = $this->getRequest();
     $user = $request->getUser();
 
     $pager = new PHUIPagerView();
     $pager->setOffset($request->getInt('page'));
 
     switch ($this->view) {
       case 'sampled':
-        $clause = '`sampleRate` > 0';
+        $clause = 'sampleRate > 0';
         $show_type = false;
         break;
       case 'my-runs':
         $clause = qsprintf(
           id(new PhabricatorXHProfSample())->establishConnection('r'),
-          '`sampleRate` = 0 AND `userPHID` = %s',
+          'sampleRate = 0 AND userPHID = %s',
           $request->getUser()->getPHID());
         $show_type = false;
         break;
       case 'manual':
-        $clause = '`sampleRate` = 0';
+        $clause = 'sampleRate = 0';
         $show_type = false;
         break;
       case 'all':
       default:
         $clause = '1 = 1';
         $show_type = true;
         break;
     }
 
     $samples = id(new PhabricatorXHProfSample())->loadAllWhere(
       '%Q ORDER BY id DESC LIMIT %d, %d',
       $clause,
       $pager->getOffset(),
       $pager->getPageSize() + 1);
 
     $samples = $pager->sliceResults($samples);
     $pager->setURI($request->getRequestURI(), 'page');
 
     $list = new PHUIObjectItemListView();
     foreach ($samples as $sample) {
       $file_phid = $sample->getFilePHID();
 
       $item = id(new PHUIObjectItemView())
         ->setObjectName($sample->getID())
         ->setHeader($sample->getRequestPath())
         ->setHref($this->getApplicationURI('profile/'.$file_phid.'/'))
         ->addAttribute(
           number_format($sample->getUsTotal())." \xCE\xBCs");
 
       if ($sample->getController()) {
         $item->addAttribute($sample->getController());
       }
 
       $item->addAttribute($sample->getHostName());
 
       $rate = $sample->getSampleRate();
       if ($rate == 0) {
         $item->addIcon('flag-6', pht('Manual Run'));
       } else {
         $item->addIcon('flag-7', pht('Sampled (1/%d)', $rate));
       }
 
       $item->addIcon(
         'none',
         phabricator_datetime($sample->getDateCreated(), $user));
 
       $list->addItem($item);
     }
 
     $list->setPager($pager);
     $list->setNoDataString(pht('There are no profiling samples.'));
 
     $crumbs = $this->buildApplicationCrumbs();
     $crumbs->addTextCrumb(pht('XHProf Samples'));
 
     return $this->buildApplicationPage(
       array($crumbs, $list),
       array(
         'title' => pht('XHProf Samples'),
       ));
 
   }
 }
diff --git a/src/docs/contributor/css_coding_standards.diviner b/src/docs/contributor/css_coding_standards.diviner
index 795d0952f..c321124ea 100644
--- a/src/docs/contributor/css_coding_standards.diviner
+++ b/src/docs/contributor/css_coding_standards.diviner
@@ -1,89 +1,91 @@
 @title CSS Coding Standards
 @group standards
 
 This document describes CSS features and coding standards for Phabricator.
 
 = Overview =
 
 This document describes technical and style guidelines for writing CSS in
 Phabricator.
 
 Phabricator has a limited CSS preprocessor. This document describes the features
 it makes available.
 
 = Z-Indexes =
 
 You should put all `z-index` rules in `z-index.css`, and keep them sorted. The
 goal is to make indexes relatively manageable and reduce the escalation of the
 Great Z-Index War where all indexes grow without bound in an endless arms race.
 
 = Color Variables =
 
 Phabricator's preprocessor provides some standard color variables. You can
 reference these with `{$color}`. For example:
 
+  lang=css
   span.critical {
     color: {$red};
   }
 
 You can find a list of all available colors in the **UIExamples** application.
 
 = Printable Rules =
 
 If you preface a rule with `!print`, it will be transformed into a print rule
 and activated when the user is printing the page or viewing a printable version
 of the page:
 
   lang=css
   !print div.menu {
     display: none;
   }
 
 Specifically, this directive causes two copies of the rule to be written out.
 The output will look something like this:
 
   lang=css
   .printable div.menu {
     display: none;
   }
 
   @media print {
     div.menu {
       display: none;
     }
   }
 
 The former will activate when users look at the printable versions of pages, by
 adding `__print__` to the URI. The latter will be activated in print contexts
 by the media query.
 
 = Device Rules =
 
 Phabricator's environment defines several device classes which can be used to
 adjust behavior responsively. In particular:
 
   lang=css
   .device-phone {
     /* Smallest breakpoint, usually for phones. */
   }
 
   .device-tablet {
     /* Middle breakpoint, usually for tablets. */
   }
 
   .device-desktop {
     /* Largest breakpoint, usually for desktops. */
   }
 
 Since many rules are specific to handheld devices, the `.device` class selects
 either tablets or phones:
 
+  lang=css
   .device {
     /* Phone or tablet (not desktop). */
   }
 
 = Image Inlining =
 
 Phabricator's CSS preprocessor automatically inlines images which are less than
 32KB using `data:` URIs. This is primarily useful for gradients or textures
 which are small and difficult to sprite.
diff --git a/src/docs/contributor/php_coding_standards.diviner b/src/docs/contributor/php_coding_standards.diviner
index 03f0bc158..ff62ebe98 100644
--- a/src/docs/contributor/php_coding_standards.diviner
+++ b/src/docs/contributor/php_coding_standards.diviner
@@ -1,169 +1,178 @@
 @title PHP Coding Standards
 @group standards
 
 This document describes PHP coding standards for Phabricator and related
 projects (like Arcanist and libphutil).
 
 = Overview =
 
 This document outlines technical and style guidelines which are followed in
 libphutil. Contributors should also follow these guidelines. Many of these
 guidelines are automatically enforced by lint.
 
 These guidelines are essentially identical to the Facebook guidelines, since I
 basically copy-pasted them. If you are already familiar with the Facebook
 guidelines, you probably don't need to read this super thoroughly.
 
 
 = Spaces, Linebreaks and Indentation =
 
   - Use two spaces for indentation. Don't use tab literal characters.
   - Use Unix linebreaks ("\n"), not MSDOS ("\r\n") or OS9 ("\r").
   - Put a space after control keywords like `if` and `for`.
   - Put a space after commas in argument lists.
   - Put a space around operators like `=`, `<`, etc.
   - Don't put spaces after function names.
   - Parentheses should hug their contents.
   - Generally, prefer to wrap code at 80 columns.
 
 = Case and Capitalization =
 
   - Name variables and functions using `lowercase_with_underscores`.
   - Name classes using `UpperCamelCase`.
   - Name methods and properties using `lowerCamelCase`.
   - Use uppercase for common acronyms like ID and HTML.
   - Name constants using `UPPERCASE`.
   - Write `true`, `false` and `null` in lowercase.
 
 = Comments =
 
   - Do not use "#" (shell-style) comments.
   - Prefer "//" comments inside function and method bodies.
 
 = PHP Language Style =
 
   - Use "<?php", not the "<?" short form. Omit the closing "?>" tag.
   - Prefer casts like `(string)` to casting functions like `strval()`.
   - Prefer type checks like `$v === null` to type functions like
     `is_null()`.
   - Avoid all crazy alternate forms of language constructs like "endwhile"
     and "<>".
   - Always put braces around conditional and loop blocks.
 
 = PHP Language Features =
 
   - Use PHP as a programming language, not a templating language.
   - Avoid globals.
   - Avoid extract().
   - Avoid eval().
   - Avoid variable variables.
   - Prefer classes over functions.
   - Prefer class constants over defines.
   - Avoid naked class properties; instead, define accessors.
   - Use exceptions for error conditions.
   - Use type hints, use `assert_instances_of()` for arrays holding objects.
 
 = Examples =
 
 **if/else:**
 
+  lang=php
   if ($some_variable > 3) {
     // ...
   } else if ($some_variable === null) {
     // ...
   } else {
     // ...
   }
 
 You should always put braces around the body of an if clause, even if it is only
 one line long. Note spaces around operators and after control statements. Do not
 use the "endif" construct, and write "else if" as two words.
 
 **for:**
 
+  lang=php
   for ($ii = 0; $ii < 10; $ii++) {
     // ...
   }
 
 Prefer $ii, $jj, $kk, etc., as iterators, since they're easier to pick out
 visually and react better to "Find Next..." in editors.
 
 **foreach:**
 
+  lang=php
   foreach ($map as $key => $value) {
     // ...
   }
 
 **switch:**
 
+  lang=php
   switch ($value) {
     case 1:
       // ...
       break;
     case 2:
       if ($flag) {
         // ...
         break;
       }
       break;
     default:
       // ...
       break;
   }
 
 `break` statements should be indented to block level.
 
 **array literals:**
 
+  lang=php
   $junk = array(
     'nuts',
     'bolts',
     'refuse',
   );
 
 Use a trailing comma and put the closing parenthesis on a separate line so that
 diffs which add elements to the array affect only one line.
 
 **operators:**
 
+  lang=php
   $a + $b;                // Put spaces around operators.
   $omg.$lol;              // Exception: no spaces around string concatenation.
   $arr[] = $element;      // Couple [] with the array when appending.
   $obj = new Thing();     // Always use parens.
 
 **function/method calls:**
 
+  lang=php
   // One line
   eject($cargo);
 
   // Multiline
   AbstractFireFactoryFactoryEngine::promulgateConflagrationInstance(
     $fuel,
     $ignition_source);
 
 **function/method definitions:**
 
+  lang=php
   function example_function($base_value, $additional_value) {
     return $base_value + $additional_value;
   }
 
   class C {
     public static function promulgateConflagrationInstance(
       IFuel $fuel,
       IgnitionSource $source) {
       // ...
     }
   }
 
 **class:**
 
+  lang=php
   class Dog extends Animal {
 
     const CIRCLES_REQUIRED_TO_LIE_DOWN = 3;
 
     private $favoriteFood = 'dirt';
 
     public function getFavoriteFood() {
       return $this->favoriteFood;
     }
   }
diff --git a/src/docs/contributor/using_oauthserver.diviner b/src/docs/contributor/using_oauthserver.diviner
index ff6f33a0c..b09f595a5 100644
--- a/src/docs/contributor/using_oauthserver.diviner
+++ b/src/docs/contributor/using_oauthserver.diviner
@@ -1,120 +1,120 @@
 @title Using the Phabricator OAuth Server
 @group developer
 
 How to use the Phabricator OAuth Server.
 
 = Overview =
 
 Phabricator includes an OAuth Server which supports the
 `Authorization Code Grant` flow as described in the OAuth 2.0
 specification:
 
 http://tools.ietf.org/html/draft-ietf-oauth-v2-23
 
 This functionality can allow clients to integrate with a given
 Phabricator instance in a secure way with granular data access.
 For example, Phabricator can be used as a central identity store for any
 clients that implement OAuth 2.0.
 
 = Vocabulary =
 
 - **Access token** - a token which allows a client to ask for data on behalf
  of a resource owner. A given client will only be able to access data included
  in the scope(s) the resource owner authorized that client for.
 - **Authorization code** - a short-lived code which allows an authenticated
  client to ask for an access token on behalf of some resource owner.
 - **Client** - this is the application or system asking for data from the
  OAuth Server on behalf of the resource owner.
 - **Resource owner** - this is the user the client and OAuth Server are
  concerned with on a given request.
 - **Scope** - this defines a specific piece of granular data a client can
  or can not access on behalf of a user. For example, if authorized for the
  "whoami" scope on behalf of a given resource owner, the client can get the
  results of Conduit.whoami for that resource owner when authenticated with
  a valid access token.
 
 = Setup - Creating a Client =
 
-# Visit https://phabricator.example.com/oauthserver/client/create/
+# Visit {nav Your Local Install > OAuth Server > Create Application}
 # Fill out the form
 # Profit
 
 = Obtaining an Authorization Code =
 
-POST or GET https://phabricator.example.com/oauthserver/auth/ with the
+POST or GET `https://phabricator.example.com/oauthserver/auth/` with the
 following parameters:
 
 - Required - **client_id** - the id of the newly registered client.
 - Required - **response_type** - the desired type of authorization code
  response. Only code is supported at this time.
 - Optional - **redirect_uri** - override the redirect_uri the client
  registered. This redirect_uri must have the same fully-qualified domain,
  path, port and have at least the same query parameters as the redirect_uri
  the client registered, as well as have no fragments.
 - Optional - **scope** - specify what scope(s) the client needs access to
  in a space-delimited list.
 - Optional - **state** - an opaque value the client can send to the server
  for programmatic excellence. Some clients use this value to implement XSRF
  protection or for debugging purposes.
 
 If done correctly and the resource owner has not yet authorized the client
 for the desired scope, then the resource owner will be presented with an
 interface to authorize the client for the desired scope. The OAuth Server
 will redirect to the pertinent redirect_uri with an authorization code or
 an error indicating the resource owner did not authorize the client, depending.
 
 If done correctly and the resource owner has already authorized the client for
 the desired scope, then the OAuth Server will redirect to the pertinent
 redirect_uri with a valid authorization code.
 
 If there is an error, the OAuth Server will return a descriptive error
 message. This error will be presented to the resource owner on the
 Phabricator domain if there is reason to believe there is something fishy
 with the client. For example, if there is an issue with the redirect_uri.
 Otherwise, the OAuth Server will redirect to the pertinent redirect_uri
 and include the pertinent error information.
 
 = Obtaining an Access Token =
 
-POST or GET https://phabricator.example.com/oauthserver/token/
+POST or GET `https://phabricator.example.com/oauthserver/token/`
 with the following parameters:
 
 - Required - **client_id** - the id of the client
 - Required - **client_secret** - the secret of the client.
  This is used to authenticate the client.
 - Required - **code** - the authorization code obtained earlier.
 - Required - **grant_type** - the desired type of access grant.
  Only token is supported at this time.
 - Optional - **redirect_uri** - should be the exact same redirect_uri as
  the redirect_uri specified to obtain the authorization code. If no
  redirect_uri was specified to obtain the authorization code then this
  should not be specified.
 
 If done correctly, the OAuth Server will redirect to the pertinent
 redirect_uri with an access token.
 
 If there is an error, the OAuth Server will return a descriptive error
 message.
 
 = Using an Access Token =
 
 Simply include a query param with the key of "access_token" and the value
 as the earlier obtained access token. For example:
 
-https://phabricator.example.com/api/user.whoami?access_token=ykc7ly7vtibj334oga4fnfbuvnwz4ocp
+```https://phabricator.example.com/api/user.whoami?access_token=ykc7ly7vtibj334oga4fnfbuvnwz4ocp```
 
 If the token has expired or is otherwise invalid, the client will receive
 an error indicating as such. In these cases, the client should re-initiate
 the entire `Authorization Code Grant` flow.
 
 NOTE: See "Scopes" section below for more information on what data is
 currently exposed through the OAuth Server.
 
 = Scopes =
 
 There are only two scopes supported at this time.
 
 - **offline_access** - allows an access token to work indefinitely without
  expiring.
 - **whoami** - allows the client to access the results of Conduit.whoami on
  behalf of the resource owner.
diff --git a/src/docs/flavor/php_pitfalls.diviner b/src/docs/flavor/php_pitfalls.diviner
index 09e0f108f..0ffcaa42d 100644
--- a/src/docs/flavor/php_pitfalls.diviner
+++ b/src/docs/flavor/php_pitfalls.diviner
@@ -1,301 +1,329 @@
 @title PHP Pitfalls
 @group php
 
 This document discusses difficult traps and pitfalls in PHP, and how to avoid,
 work around, or at least understand them.
 
-= array_merge() in Incredibly Slow When Merging A List of Arrays =
+= `array_merge()` in Incredibly Slow When Merging A List of Arrays =
 
 If you merge a list of arrays like this:
 
-  COUNTEREXAMPLE
+  COUNTEREXAMPLE, lang=php
   $result = array();
   foreach ($list_of_lists as $one_list) {
     $result = array_merge($result, $one_list);
   }
 
 ...your program now has a huge runtime because it generates a large number of
 intermediate arrays and copies every element it has previously seen each time
 you iterate.
 
 In a libphutil environment, you can use @{function@libphutil:array_mergev}
 instead.
 
-= var_export() Hates Baby Animals =
+= `var_export()` Hates Baby Animals =
 
-If you try to var_export() an object that contains recursive references, your
+If you try to `var_export()` an object that contains recursive references, your
 program will terminate. You have no chance to intercept or react to this or
-otherwise stop it from happening. Avoid var_export() unless you are certain
-you have only simple data. You can use print_r() or var_dump() to display
+otherwise stop it from happening. Avoid `var_export()` unless you are certain
+you have only simple data. You can use `print_r()` or `var_dump()` to display
 complex variables safely.
 
-= isset(), empty() and Truthiness =
+= `isset()`, `empty()` and Truthiness =
 
 A value is "truthy" if it evaluates to true in an `if` clause:
 
+  lang=php
   $value = something();
   if ($value) {
     // Value is truthy.
   }
 
 If a value is not truthy, it is "falsey". These values are falsey in PHP:
 
   null      // null
   0         // integer
   0.0       // float
   "0"       // string
   ""        // empty string
   false     // boolean
   array()   // empty array
 
 Disregarding some bizarre edge cases, all other values are truthy. Note that
 because "0" is falsey, this sort of thing (intended to prevent users from making
 empty comments) is wrong in PHP:
 
   COUNTEREXAMPLE
   if ($comment_text) {
     make_comment($comment_text);
   }
 
 This is wrong because it prevents users from making the comment "0". //THIS
 COMMENT IS TOTALLY AWESOME AND I MAKE IT ALL THE TIME SO YOU HAD BETTER NOT
-BREAK IT!!!// A better test is probably strlen().
+BREAK IT!!!// A better test is probably `strlen()`.
 
 In addition to truth tests with `if`, PHP has two special truthiness operators
-which look like functions but aren't: empty() and isset(). These operators help
-deal with undeclared variables.
+which look like functions but aren't: `empty()` and `isset()`. These operators
+help deal with undeclared variables.
 
 In PHP, there are two major cases where you get undeclared variables -- either
 you directly use a variable without declaring it:
 
-  COUNTEREXAMPLE
+  COUNTEREXAMPLE, lang=php
   function f() {
     if ($not_declared) {
       // ...
     }
   }
 
 ...or you index into an array with an index which may not exist:
 
   COUNTEREXAMPLE
   function f(array $mystery) {
     if ($mystery['stuff']) {
       // ...
     }
   }
 
-When you do either of these, PHP issues a warning. Avoid these warnings by using
-empty() and isset() to do tests that are safe to apply to undeclared variables.
-
-empty() evaluates truthiness exactly opposite of if(). isset() returns true for
-everything except null. This is the truth table:
-
-  VALUE             if()        empty()     isset()
-
-  null              false       true        false
-  0                 false       true        true
-  0.0               false       true        true
-  "0"               false       true        true
-  ""                false       true        true
-  false             false       true        true
-  array()           false       true        true
-  EVERYTHING ELSE   true        false       true
-
-The value of these operators is that they accept undeclared variables and do not
-issue a warning. Specifically, if you try to do this you get a warning:
-
-  COUNTEREXAMPLE
-  if ($not_previously_declared) {         // PHP Notice:  Undefined variable!
-    // ...
-  }
+When you do either of these, PHP issues a warning. Avoid these warnings by
+using `empty()` and `isset()` to do tests that are safe to apply to undeclared
+variables.
+
+`empty()` evaluates truthiness exactly opposite of `if()`. `isset()` returns
+`true` for everything except `null`. This is the truth table:
+
+| Value | `if()` | `empty()` | `isset()` |
+|-------|--------|-----------|-----------|
+| `null` | `false` | `true` | `false` |
+| `0` | `false` | `true` | `true` |
+| `0.0` | `false` | `true` | `true` |
+| `"0"` | `false` | `true` | `true` |
+| `""` | `false` | `true` | `true` |
+| `false` | `false` | `true` | `true` |
+| `array()` | `false` | `true` | `true` |
+| Everything else | `true` | `false` | `true` |
+
+The value of these operators is that they accept undeclared variables and do
+not issue a warning. Specifically, if you try to do this you get a warning:
+
+```lang=php, COUNTEREXAMPLE
+if ($not_previously_declared) {         // PHP Notice:  Undefined variable!
+  // ...
+}
+```
 
 But these are fine:
 
-  if (empty($not_previously_declared)) {  // No notice, returns true.
-    // ...
-  }
-  if (isset($not_previously_declared)) {  // No notice, returns false.
-    // ...
-  }
-
-So, isset() really means is_declared_and_is_set_to_something_other_than_null().
-empty() really means is_falsey_or_is_not_declared(). Thus:
-
-  - If a variable is known to exist, test falsiness with if (!$v), not empty().
-    In particular, test for empty arrays with if (!$array). There is no reason
-    to ever use empty() on a declared variable.
-  - When you use isset() on an array key, like isset($array['key']), it will
-    evaluate to "false" if the key exists but has the value null! Test for index
-    existence with array_key_exists().
-
-Put another way, use isset() if you want to type "if ($value !== null)" but are
-testing something that may not be declared. Use empty() if you want to type
-"if (!$value)" but you are testing something that may not be declared.
+```lang=php
+if (empty($not_previously_declared)) {  // No notice, returns true.
+  // ...
+}
+if (isset($not_previously_declared)) {  // No notice, returns false.
+  // ...
+}
+```
+
+So, `isset()` really means
+`is_declared_and_is_set_to_something_other_than_null()`. `empty()` really means
+`is_falsey_or_is_not_declared()`. Thus:
+
+  - If a variable is known to exist, test falsiness with `if (!$v)`, not
+    `empty()`. In particular, test for empty arrays with `if (!$array)`. There
+    is no reason to ever use `empty()` on a declared variable.
+  - When you use `isset()` on an array key, like `isset($array['key'])`, it
+    will evaluate to "false" if the key exists but has the value `null`! Test
+    for index existence with `array_key_exists()`.
+
+Put another way, use `isset()` if you want to type `if ($value !== null)` but
+are testing something that may not be declared. Use `empty()` if you want to
+type `if (!$value)` but you are testing something that may not be declared.
 
 = usort(), uksort(), and uasort() are Slow =
 
 This family of functions is often extremely slow for large datasets. You should
 avoid them if at all possible. Instead, build an array which contains surrogate
 keys that are naturally sortable with a function that uses native comparison
-(e.g., sort(), asort(), ksort(), or natcasesort()). Sort this array instead, and
-use it to reorder the original array.
+(e.g., `sort()`, `asort()`, `ksort()`, or `natcasesort()`). Sort this array
+instead, and use it to reorder the original array.
 
 In a libphutil environment, you can often do this easily with
 @{function@libphutil:isort} or @{function@libphutil:msort}.
 
-= array_intersect() and array_diff() are Also Slow =
+= `array_intersect()` and `array_diff()` are Also Slow =
 
 These functions are much slower for even moderately large inputs than
-array_intersect_key() and array_diff_key(), because they can not make the
+`array_intersect_key()` and `array_diff_key()`, because they can not make the
 assumption that their inputs are unique scalars as the `key` varieties can.
 Strongly prefer the `key` varieties.
 
-= array_uintersect() and array_udiff() are Definitely Slow Too =
+= `array_uintersect()` and `array_udiff()` are Definitely Slow Too =
 
 These functions have the problems of both the `usort()` family and the
 `array_diff()` family. Avoid them.
 
-= foreach() Does Not Create Scope =
+= `foreach()` Does Not Create Scope =
 
-Variables survive outside of the scope of foreach(). More problematically,
-references survive outside of the scope of foreach(). This code mutates
+Variables survive outside of the scope of `foreach()`. More problematically,
+references survive outside of the scope of `foreach()`. This code mutates
 `$array` because the reference leaks from the first loop to the second:
 
-  COUNTEREXAMPLE
-  $array = range(1, 3);
-  echo implode(',', $array); // Outputs '1,2,3'
-  foreach ($array as &$value) {}
-  echo implode(',', $array); // Outputs '1,2,3'
-  foreach ($array as $value) {}
-  echo implode(',', $array); // Outputs '1,2,2'
+```lang=php, COUNTEREXAMPLE
+$array = range(1, 3);
+echo implode(',', $array); // Outputs '1,2,3'
+foreach ($array as &$value) {}
+echo implode(',', $array); // Outputs '1,2,3'
+foreach ($array as $value) {}
+echo implode(',', $array); // Outputs '1,2,2'
+```
 
 The easiest way to avoid this is to avoid using foreach-by-reference. If you do
 use it, unset the reference after the loop:
 
-  foreach ($array as &$value) {
-    // ...
-  }
-  unset($value);
-
-= unserialize() is Incredibly Slow on Large Datasets =
+```lang=php
+foreach ($array as &$value) {
+  // ...
+}
+unset($value);
+```
 
-The performance of unserialize() is nonlinear in the number of zvals you
-unserialize, roughly O(N^2).
+= `unserialize()` is Incredibly Slow on Large Datasets =
 
-  zvals       approximate time
-  10000       5ms
-  100000      85ms
-  1000000     8,000ms
-  10000000    72 billion years
+The performance of `unserialize()` is nonlinear in the number of zvals you
+unserialize, roughly `O(N^2)`.
 
+| zvals | Approximate time |
+|-------|------------------|
+| 10000 |5ms |
+| 100000 | 85ms |
+| 1000000 | 8,000ms |
+| 10000000 | 72 billion years |
 
-= call_user_func() Breaks References =
+= `call_user_func()` Breaks References =
 
-If you use call_use_func() to invoke a function which takes parameters by
+If you use `call_use_func()` to invoke a function which takes parameters by
 reference, the variables you pass in will have their references broken and will
 emerge unmodified. That is, if you have a function that takes references:
 
-  function add_one(&$v) {
-    $v++;
-  }
+```lang=php
+function add_one(&$v) {
+  $v++;
+}
+```
 
-...and you call it with call_user_func():
+...and you call it with `call_user_func()`:
 
-  COUNTEREXAMPLE
-  $x = 41;
-  call_user_func('add_one', $x);
+```lang=php, COUNTEREXAMPLE
+$x = 41;
+call_user_func('add_one', $x);
+```
 
-...`$x` will not be modified. The solution is to use call_user_func_array()
+...`$x` will not be modified. The solution is to use `call_user_func_array()`
 and wrap the reference in an array:
 
-  $x = 41;
-  call_user_func_array(
-    'add_one',
-    array(&$x)); // Note '&$x'!
+```lang=php
+$x = 41;
+call_user_func_array(
+  'add_one',
+  array(&$x)); // Note '&$x'!
+```
 
 This will work as expected.
 
-= You Can't Throw From __toString() =
+= You Can't Throw From `__toString()` =
 
-If you throw from __toString(), your program will terminate uselessly and you
+If you throw from `__toString()`, your program will terminate uselessly and you
 won't get the exception.
 
 = An Object Can Have Any Scalar as a Property =
 
 Object properties are not limited to legal variable names:
 
-  $property = '!@#$%^&*()';
-  $obj->$property = 'zebra';
-  echo $obj->$property;       // Outputs 'zebra'.
+```lang=php
+$property = '!@#$%^&*()';
+$obj->$property = 'zebra';
+echo $obj->$property;       // Outputs 'zebra'.
+```
 
 So, don't make assumptions about property names.
 
-= There is an (object) Cast =
+= There is an `(object)` Cast =
 
 You can cast a dictionary into an object.
 
-  $obj = (object)array('flavor' => 'coconut');
-  echo $obj->flavor;      // Outputs 'coconut'.
-  echo get_class($obj);   // Outputs 'stdClass'.
+```lang=php
+$obj = (object)array('flavor' => 'coconut');
+echo $obj->flavor;      // Outputs 'coconut'.
+echo get_class($obj);   // Outputs 'stdClass'.
+```
 
 This is occasionally useful, mostly to force an object to become a Javascript
-dictionary (vs a list) when passed to json_encode().
+dictionary (vs a list) when passed to `json_encode()`.
 
-= Invoking "new" With an Argument Vector is Really Hard =
+= Invoking `new` With an Argument Vector is Really Hard =
 
-If you have some `$class_name` and some `$argv` of constructor
-arguments and you want to do this:
+If you have some `$class_name` and some `$argv` of constructor arguments
+and you want to do this:
 
-  new $class_name($argv[0], $argv[1], ...);
+```lang=php
+new $class_name($argv[0], $argv[1], ...);
+```
 
 ...you'll probably invent a very interesting, very novel solution that is very
 wrong. In a libphutil environment, solve this problem with
-@{function@libphutil:newv}. Elsewhere, copy newv()'s implementation.
+@{function@libphutil:newv}. Elsewhere, copy `newv()`'s implementation.
 
 = Equality is not Transitive =
 
 This isn't terribly surprising since equality isn't transitive in a lot of
-languages, but the == operator is not transitive:
+languages, but the `==` operator is not transitive:
 
-  $a = ''; $b = 0; $c = '0a';
-  $a == $b; // true
-  $b == $c; // true
-  $c == $a; // false!
+```lang=php
+$a = ''; $b = 0; $c = '0a';
+$a == $b; // true
+$b == $c; // true
+$c == $a; // false!
+```
 
 When either operand is an integer, the other operand is cast to an integer
-before comparison. Avoid this and similar pitfalls by using the === operator,
+before comparison. Avoid this and similar pitfalls by using the `===` operator,
 which is transitive.
 
 = All 676 Letters in the Alphabet =
 
 This doesn't do what you'd expect it to do in C:
 
-  for ($c = 'a'; $c <= 'z'; $c++) {
-    // ...
-  }
-
-This is because the successor to 'z' is 'aa', which is "less than" 'z'. The
-loop will run for ~700 iterations until it reaches 'zz' and terminates. That is,
-`$c` will take on these values:
-
-  a
-  b
-  ...
-  y
-  z
-  aa // loop continues because 'aa' <= 'z'
-  ab
-  ...
-  mf
-  mg
-  ...
-  zw
-  zx
-  zy
-  zz // loop now terminates because 'zz' > 'z'
+```lang=php
+for ($c = 'a'; $c <= 'z'; $c++) {
+  // ...
+}
+```
+
+This is because the successor to `z` is `aa`, which is "less than" `z`.
+The loop will run for ~700 iterations until it reaches `zz` and terminates.
+That is, `$c` will take on these values:
+
+```
+a
+b
+...
+y
+z
+aa // loop continues because 'aa' <= 'z'
+ab
+...
+mf
+mg
+...
+zw
+zx
+zy
+zz // loop now terminates because 'zz' > 'z'
+```
 
 Instead, use this loop:
 
-  foreach (range('a', 'z') as $c) {
-    // ...
-  }
+```lang=php
+foreach (range('a', 'z') as $c) {
+  // ...
+}
+```
diff --git a/src/docs/user/userguide/spaces.diviner b/src/docs/user/userguide/spaces.diviner
new file mode 100644
index 000000000..230fa2ddc
--- /dev/null
+++ b/src/docs/user/userguide/spaces.diviner
@@ -0,0 +1,169 @@
+@title Spaces User Guide
+@group userguide
+
+Guide to the Spaces application.
+
+Overview
+========
+
+IMPORTANT: Spaces is a prototype application.
+
+The Spaces application makes it easier to manage large groups of objects which
+share the same access policy. For example:
+
+  - An organization might make a Space for a project in order to satisfy a
+    contractual obligation to limit access, even internally.
+  - An open source organization might make a Space for work related to
+    internal governance, to separate private and public discussions.
+  - A contracting company might make Spaces for clients, to separate them from
+    one another.
+  - A company might create a Space for consultants, to give them limited
+    access to only the resources they need to do their work.
+  - An ambitious manager might create a Space to hide her team's work from her
+    enemies at the company, that she might use the element of surprise to later
+    expand her domain.
+
+Phabricator's access control policies are generally powerful enough to handle
+these use cases on their own, but applying the same policy to a large group
+of objects requires a lot of effort and is error-prone.
+
+Spaces build on top of policies and make it easier and more reliable to
+configure, review, and manage groups of objects with similar policies.
+
+
+Creating Spaces
+=================
+
+Spaces are optional, and are inactive by default. You don't need to configure
+them if you don't plan to use them. You can always set them up later.
+
+To activate Spaces, you need to create at least two spaces. Create spaces from
+the web UI, by navigating to {nav Spaces > Create Space}. By default, only
+administrators can create new Spaces, but you can configure this in the
+{nav Applications} application.
+
+The first Space you create will be a special "default" Space, and all existing
+objects will be shifted into this space as soon as you create it. Spaces you
+create later will be normal spaces, and begin with no objects inside them.
+
+Create the first space (you may want to name it something like "Default" or
+"Global" or "Public", depending on the nature of your organization), then
+create a second Space. Usually, the second space will be something like
+"Secret Plans" and have a more restrictive "Visible To" policy.
+
+
+Using Spaces
+============
+
+Once you've created at least two spaces, you can begin using them.
+
+Application UIs will change for users who can see at least two Spaces, opening
+up new controls which let them work with spaces. They will now be able to
+choose which space to create new objects into, be able to move objects between
+spaces, and be able to search for objects in a specific space or set of spaces.
+
+In list and detail views, objects will show which space they're in if they're
+in a non-default space.
+
+Users with access to only one space won't see these controls, even if many
+spaces exist. This simplifies the UI for users with limited access.
+
+
+Space Policies
+==============
+
+Briefly, Spaces affect policies like this:
+
+  - Spaces apply their view policy to all objects inside the space.
+  - Space policies are absolute, and stronger than all other policies. A
+    user who can not see a Space can **never** see objects inside the space.
+  - Normal policies are still checked: spaces can only reduce access.
+
+When you create a Space, you choose a view policy for that space by using the
+**Visible To** control. This policy controls both who can see the space, and
+who can see objects inside the space.
+
+Spaces apply their view policy to all objects inside the space: if you can't
+see a space, you can never see objects inside it. This policy check is absolute
+and stronger than all other policy rules, including policy exceptions.
+
+For example, a user can never see a task in a space they can't see, even if
+they are an admin and the author and owner of the task, and subscribed to the
+task and the view and edit policies are set to "All Users", and they created
+the Space originally and the moon is full and they are pure of heart and
+possessed of the noblest purpose. Spaces are impenetrable.
+
+Even if a user satisfies the view policy for a space, they must still pass the
+view policy on the object: the space check is a new check in addition to any
+check on the object, and can only limit access.
+
+The edit policy for a space only affects the Space itself, and is not applied
+to objects inside the space.
+
+
+Archiving Spaces
+================
+
+If you no longer need a Space, you can archive it by choosing
+{nav Archive Space} from the detail view. This hides the space and all the
+objects in it without deleting any data.
+
+New objects can't be created into archived spaces, and existing objects can't
+be shifted into archived spaces. The UI won't give you options to choose
+these spaces when creating or editing objects.
+
+Additionally, objects (like tasks) in archived spaces won't be shown in most
+search result lists by default. If you need to find objects in an archived
+space, use the `Spaces` constraint to specifically search for objects in that
+space.
+
+You can reactivate a space later by choosing {nav Activate Space}.
+
+
+Application Email
+=================
+
+After activating Spaces, you can choose a Space when configuring inbound email
+addresses in {nav Applications}.
+
+Spaces affect policies for application email just like they do for other
+objects: to see or use the address, you must be able to see the space which
+contains it.
+
+Objects created from inbound email will be created in the Space the email is
+associated with.
+
+
+Limitations and Caveats
+=======================
+
+Some information is shared between spaces, so they do not completely isolate
+users from other activity on the install. This section discusses limitations
+of the isolation model. Most of these limitations are intrinsic to the policy
+model Phabricator uses.
+
+**Shared IDs**: Spaces do not have unique object IDs: there is only one `T1`,
+not a separate one in each space. It can be moved between spaces, but `T1`
+always refers to the same object. In most cases, this makes working with
+spaces simpler and easier.
+
+However, because IDs are shared, users in any space can look at object IDs to
+determine how many objects exist in other spaces, even if they can't see those
+objects. If a user creates a new task and sees that it is `T5000`, they can
+know that there are 4,999 other tasks they don't have permission to see.
+
+**Globally Unique Values**: Some values (like usernames, email addresses,
+project hashtags, repository callsigns, and application emails) must be
+globally unique.
+
+As with normal policies, users may be able to determine that a `#yolo` project
+exists, even if they can't see it: they can try to create a project using the
+`#yolo` hashtag, and will receive an error if it is a duplicate.
+
+**User Accounts**: Spaces do not apply to users, and can not hide the existence
+of user accounts.
+
+For example, if you are a contracting company and have Coke and Pepsi as
+clients, the CEO of Coke and the CEO of Pepsi will each be able to see that the
+other has an account on the install, even if all the work you are doing for
+them is separated into "Coke" and "Pepsi" spaces.
diff --git a/src/infrastructure/customfield/field/PhabricatorCustomField.php b/src/infrastructure/customfield/field/PhabricatorCustomField.php
index 4b0f68e93..f9f597902 100644
--- a/src/infrastructure/customfield/field/PhabricatorCustomField.php
+++ b/src/infrastructure/customfield/field/PhabricatorCustomField.php
@@ -1,1362 +1,1362 @@
 <?php
 
 /**
  * @task apps         Building Applications with Custom Fields
  * @task core         Core Properties and Field Identity
  * @task proxy        Field Proxies
  * @task context      Contextual Data
  * @task render       Rendering Utilities
  * @task storage      Field Storage
  * @task edit         Integration with Edit Views
  * @task view         Integration with Property Views
  * @task list         Integration with List views
  * @task appsearch    Integration with ApplicationSearch
  * @task appxaction   Integration with ApplicationTransactions
  * @task xactionmail  Integration with Transaction Mail
  * @task globalsearch Integration with Global Search
  * @task herald       Integration with Herald
  */
-abstract class PhabricatorCustomField {
+abstract class PhabricatorCustomField extends Phobject {
 
   private $viewer;
   private $object;
   private $proxy;
 
   const ROLE_APPLICATIONTRANSACTIONS  = 'ApplicationTransactions';
   const ROLE_TRANSACTIONMAIL          = 'ApplicationTransactions.mail';
   const ROLE_APPLICATIONSEARCH        = 'ApplicationSearch';
   const ROLE_STORAGE                  = 'storage';
   const ROLE_DEFAULT                  = 'default';
   const ROLE_EDIT                     = 'edit';
   const ROLE_VIEW                     = 'view';
   const ROLE_LIST                     = 'list';
   const ROLE_GLOBALSEARCH             = 'GlobalSearch';
   const ROLE_CONDUIT                  = 'conduit';
   const ROLE_HERALD                   = 'herald';
 
 
 /* -(  Building Applications with Custom Fields  )--------------------------- */
 
 
   /**
    * @task apps
    */
   public static function getObjectFields(
     PhabricatorCustomFieldInterface $object,
     $role) {
 
     try {
       $attachment = $object->getCustomFields();
     } catch (PhabricatorDataNotAttachedException $ex) {
       $attachment = new PhabricatorCustomFieldAttachment();
       $object->attachCustomFields($attachment);
     }
 
     try {
       $field_list = $attachment->getCustomFieldList($role);
     } catch (PhabricatorCustomFieldNotAttachedException $ex) {
       $base_class = $object->getCustomFieldBaseClass();
 
       $spec = $object->getCustomFieldSpecificationForRole($role);
       if (!is_array($spec)) {
         throw new Exception(
           pht(
             "Expected an array from %s for object of class '%s'.",
             'getCustomFieldSpecificationForRole()',
             get_class($object)));
       }
 
       $fields = self::buildFieldList(
         $base_class,
         $spec,
         $object);
 
       foreach ($fields as $key => $field) {
         if (!$field->shouldEnableForRole($role)) {
           unset($fields[$key]);
         }
       }
 
       foreach ($fields as $field) {
         $field->setObject($object);
       }
 
       $field_list = new PhabricatorCustomFieldList($fields);
       $attachment->addCustomFieldList($role, $field_list);
     }
 
     return $field_list;
   }
 
 
   /**
    * @task apps
    */
   public static function getObjectField(
     PhabricatorCustomFieldInterface $object,
     $role,
     $field_key) {
 
     $fields = self::getObjectFields($object, $role)->getFields();
 
     return idx($fields, $field_key);
   }
 
 
   /**
    * @task apps
    */
   public static function buildFieldList(
     $base_class,
     array $spec,
     $object,
     array $options = array()) {
 
     PhutilTypeSpec::checkMap(
       $options,
       array(
         'withDisabled' => 'optional bool',
       ));
 
     $field_objects = id(new PhutilSymbolLoader())
       ->setAncestorClass($base_class)
       ->loadObjects();
 
     $fields = array();
     $from_map = array();
     foreach ($field_objects as $field_object) {
       $current_class = get_class($field_object);
       foreach ($field_object->createFields($object) as $field) {
         $key = $field->getFieldKey();
         if (isset($fields[$key])) {
           throw new Exception(
             pht(
               "Both '%s' and '%s' define a custom field with ".
               "field key '%s'. Field keys must be unique.",
               $from_map[$key],
               $current_class,
               $key));
         }
         $from_map[$key] = $current_class;
         $fields[$key] = $field;
       }
     }
 
     foreach ($fields as $key => $field) {
       if (!$field->isFieldEnabled()) {
         unset($fields[$key]);
       }
     }
 
     $fields = array_select_keys($fields, array_keys($spec)) + $fields;
 
     if (empty($options['withDisabled'])) {
       foreach ($fields as $key => $field) {
         $config = idx($spec, $key, array()) + array(
           'disabled' => $field->shouldDisableByDefault(),
         );
 
         if (!empty($config['disabled'])) {
           if ($field->canDisableField()) {
             unset($fields[$key]);
           }
         }
       }
     }
 
     return $fields;
   }
 
 
 /* -(  Core Properties and Field Identity  )--------------------------------- */
 
 
   /**
    * Return a key which uniquely identifies this field, like
    * "mycompany:dinosaur:count". Normally you should provide some level of
    * namespacing to prevent collisions.
    *
    * @return string String which uniquely identifies this field.
    * @task core
    */
   public function getFieldKey() {
     if ($this->proxy) {
       return $this->proxy->getFieldKey();
     }
     throw new PhabricatorCustomFieldImplementationIncompleteException(
       $this,
       $field_key_is_incomplete = true);
   }
 
 
   /**
    * Return a human-readable field name.
    *
    * @return string Human readable field name.
    * @task core
    */
   public function getFieldName() {
     if ($this->proxy) {
       return $this->proxy->getFieldName();
     }
     return $this->getFieldKey();
   }
 
 
   /**
    * Return a short, human-readable description of the field's behavior. This
    * provides more context to administrators when they are customizing fields.
    *
    * @return string|null Optional human-readable description.
    * @task core
    */
   public function getFieldDescription() {
     if ($this->proxy) {
       return $this->proxy->getFieldDescription();
     }
     return null;
   }
 
 
   /**
    * Most field implementations are unique, in that one class corresponds to
    * one field. However, some field implementations are general and a single
    * implementation may drive several fields.
    *
    * For general implementations, the general field implementation can return
    * multiple field instances here.
    *
    * @param object The object to create fields for.
    * @return list<PhabricatorCustomField> List of fields.
    * @task core
    */
   public function createFields($object) {
     return array($this);
   }
 
 
   /**
    * You can return `false` here if the field should not be enabled for any
    * role. For example, it might depend on something (like an application or
    * library) which isn't installed, or might have some global configuration
    * which allows it to be disabled.
    *
    * @return bool False to completely disable this field for all roles.
    * @task core
    */
   public function isFieldEnabled() {
     if ($this->proxy) {
       return $this->proxy->isFieldEnabled();
     }
     return true;
   }
 
 
   /**
    * Low level selector for field availability. Fields can appear in different
    * roles (like an edit view, a list view, etc.), but not every field needs
    * to appear everywhere. Fields that are disabled in a role won't appear in
    * that context within applications.
    *
    * Normally, you do not need to override this method. Instead, override the
    * methods specific to roles you want to enable. For example, implement
    * @{method:shouldUseStorage()} to activate the `'storage'` role.
    *
    * @return bool True to enable the field for the given role.
    * @task core
    */
   public function shouldEnableForRole($role) {
 
     // NOTE: All of these calls proxy individually, so we don't need to
     // proxy this call as a whole.
 
     switch ($role) {
       case self::ROLE_APPLICATIONTRANSACTIONS:
         return $this->shouldAppearInApplicationTransactions();
       case self::ROLE_APPLICATIONSEARCH:
         return $this->shouldAppearInApplicationSearch();
       case self::ROLE_STORAGE:
         return $this->shouldUseStorage();
       case self::ROLE_EDIT:
         return $this->shouldAppearInEditView();
       case self::ROLE_VIEW:
         return $this->shouldAppearInPropertyView();
       case self::ROLE_LIST:
         return $this->shouldAppearInListView();
       case self::ROLE_GLOBALSEARCH:
         return $this->shouldAppearInGlobalSearch();
       case self::ROLE_CONDUIT:
         return $this->shouldAppearInConduitDictionary();
       case self::ROLE_TRANSACTIONMAIL:
         return $this->shouldAppearInTransactionMail();
       case self::ROLE_HERALD:
         return $this->shouldAppearInHerald();
       case self::ROLE_DEFAULT:
         return true;
       default:
         throw new Exception(pht("Unknown field role '%s'!", $role));
     }
   }
 
 
   /**
    * Allow administrators to disable this field. Most fields should allow this,
    * but some are fundamental to the behavior of the application and can be
    * locked down to avoid chaos, disorder, and the decline of civilization.
    *
    * @return bool False to prevent this field from being disabled through
    *              configuration.
    * @task core
    */
   public function canDisableField() {
     return true;
   }
 
   public function shouldDisableByDefault() {
     return false;
   }
 
 
   /**
    * Return an index string which uniquely identifies this field.
    *
    * @return string Index string which uniquely identifies this field.
    * @task core
    */
   final public function getFieldIndex() {
     return PhabricatorHash::digestForIndex($this->getFieldKey());
   }
 
 
 /* -(  Field Proxies  )------------------------------------------------------ */
 
 
   /**
    * Proxies allow a field to use some other field's implementation for most
    * of their behavior while still subclassing an application field. When a
    * proxy is set for a field with @{method:setProxy}, all of its methods will
    * call through to the proxy by default.
    *
    * This is most commonly used to implement configuration-driven custom fields
    * using @{class:PhabricatorStandardCustomField}.
    *
    * This method must be overridden to return `true` before a field can accept
    * proxies.
    *
    * @return bool True if you can @{method:setProxy} this field.
    * @task proxy
    */
   public function canSetProxy() {
     if ($this instanceof PhabricatorStandardCustomFieldInterface) {
       return true;
     }
     return false;
   }
 
 
   /**
    * Set the proxy implementation for this field. See @{method:canSetProxy} for
    * discussion of field proxies.
    *
    * @param PhabricatorCustomField Field implementation.
    * @return this
    */
   final public function setProxy(PhabricatorCustomField $proxy) {
     if (!$this->canSetProxy()) {
       throw new PhabricatorCustomFieldNotProxyException($this);
     }
 
     $this->proxy = $proxy;
     return $this;
   }
 
 
   /**
    * Get the field's proxy implementation, if any. For discussion, see
    * @{method:canSetProxy}.
    *
    * @return PhabricatorCustomField|null  Proxy field, if one is set.
    */
   final public function getProxy() {
     return $this->proxy;
   }
 
 
 /* -(  Contextual Data  )---------------------------------------------------- */
 
 
   /**
    * Sets the object this field belongs to.
    *
    * @param PhabricatorCustomFieldInterface The object this field belongs to.
    * @return this
    * @task context
    */
   final public function setObject(PhabricatorCustomFieldInterface $object) {
     if ($this->proxy) {
       $this->proxy->setObject($object);
       return $this;
     }
 
     $this->object = $object;
     $this->didSetObject($object);
     return $this;
   }
 
 
   /**
    * Read object data into local field storage, if applicable.
    *
    * @param PhabricatorCustomFieldInterface The object this field belongs to.
    * @return this
    * @task context
    */
   public function readValueFromObject(PhabricatorCustomFieldInterface $object) {
     if ($this->proxy) {
       $this->proxy->readValueFromObject($object);
     }
     return $this;
   }
 
 
   /**
    * Get the object this field belongs to.
    *
    * @return PhabricatorCustomFieldInterface The object this field belongs to.
    * @task context
    */
   final public function getObject() {
     if ($this->proxy) {
       return $this->proxy->getObject();
     }
 
     return $this->object;
   }
 
 
   /**
    * This is a hook, primarily for subclasses to load object data.
    *
    * @return PhabricatorCustomFieldInterface The object this field belongs to.
    * @return void
    */
   protected function didSetObject(PhabricatorCustomFieldInterface $object) {
     return;
   }
 
 
   /**
    * @task context
    */
   final public function setViewer(PhabricatorUser $viewer) {
     if ($this->proxy) {
       $this->proxy->setViewer($viewer);
       return $this;
     }
 
     $this->viewer = $viewer;
     return $this;
   }
 
 
   /**
    * @task context
    */
   final public function getViewer() {
     if ($this->proxy) {
       return $this->proxy->getViewer();
     }
 
     return $this->viewer;
   }
 
 
   /**
    * @task context
    */
   final protected function requireViewer() {
     if ($this->proxy) {
       return $this->proxy->requireViewer();
     }
 
     if (!$this->viewer) {
       throw new PhabricatorCustomFieldDataNotAvailableException($this);
     }
     return $this->viewer;
   }
 
 
 /* -(  Rendering Utilities  )------------------------------------------------ */
 
 
   /**
    * @task render
    */
   protected function renderHandleList(array $handles) {
     if (!$handles) {
       return null;
     }
 
     $out = array();
     foreach ($handles as $handle) {
       $out[] = $handle->renderLink();
     }
 
     return phutil_implode_html(phutil_tag('br'), $out);
   }
 
 
 /* -(  Storage  )------------------------------------------------------------ */
 
 
   /**
    * Return true to use field storage.
    *
    * Fields which can be edited by the user will most commonly use storage,
    * while some other types of fields (for instance, those which just display
    * information in some stylized way) may not. Many builtin fields do not use
    * storage because their data is available on the object itself.
    *
    * If you implement this, you must also implement @{method:getValueForStorage}
    * and @{method:setValueFromStorage}.
    *
    * @return bool True to use storage.
    * @task storage
    */
   public function shouldUseStorage() {
     if ($this->proxy) {
       return $this->proxy->shouldUseStorage();
     }
     return false;
   }
 
 
   /**
    * Return a new, empty storage object. This should be a subclass of
    * @{class:PhabricatorCustomFieldStorage} which is bound to the application's
    * database.
    *
    * @return PhabricatorCustomFieldStorage New empty storage object.
    * @task storage
    */
   public function newStorageObject() {
     if ($this->proxy) {
       return $this->proxy->newStorageObject();
     }
     throw new PhabricatorCustomFieldImplementationIncompleteException($this);
   }
 
 
   /**
    * Return a serialized representation of the field value, appropriate for
    * storing in auxiliary field storage. You must implement this method if
    * you implement @{method:shouldUseStorage}.
    *
    * If the field value is a scalar, it can be returned unmodiifed. If not,
    * it should be serialized (for example, using JSON).
    *
    * @return string Serialized field value.
    * @task storage
    */
   public function getValueForStorage() {
     if ($this->proxy) {
       return $this->proxy->getValueForStorage();
     }
     throw new PhabricatorCustomFieldImplementationIncompleteException($this);
   }
 
 
   /**
    * Set the field's value given a serialized storage value. This is called
    * when the field is loaded; if no data is available, the value will be
    * null. You must implement this method if you implement
    * @{method:shouldUseStorage}.
    *
    * Usually, the value can be loaded directly. If it isn't a scalar, you'll
    * need to undo whatever serialization you applied in
    * @{method:getValueForStorage}.
    *
    * @param string|null Serialized field representation (from
    *                    @{method:getValueForStorage}) or null if no value has
    *                    ever been stored.
    * @return this
    * @task storage
    */
   public function setValueFromStorage($value) {
     if ($this->proxy) {
       return $this->proxy->setValueFromStorage($value);
     }
     throw new PhabricatorCustomFieldImplementationIncompleteException($this);
   }
 
 
 /* -(  ApplicationSearch  )-------------------------------------------------- */
 
 
   /**
    * Appearing in ApplicationSearch allows a field to be indexed and searched
    * for.
    *
    * @return bool True to appear in ApplicationSearch.
    * @task appsearch
    */
   public function shouldAppearInApplicationSearch() {
     if ($this->proxy) {
       return $this->proxy->shouldAppearInApplicationSearch();
     }
     return false;
   }
 
 
   /**
    * Return one or more indexes which this field can meaningfully query against
    * to implement ApplicationSearch.
    *
    * Normally, you should build these using @{method:newStringIndex} and
    * @{method:newNumericIndex}. For example, if a field holds a numeric value
    * it might return a single numeric index:
    *
    *   return array($this->newNumericIndex($this->getValue()));
    *
    * If a field holds a more complex value (like a list of users), it might
    * return several string indexes:
    *
    *   $indexes = array();
    *   foreach ($this->getValue() as $phid) {
    *     $indexes[] = $this->newStringIndex($phid);
    *   }
    *   return $indexes;
    *
    * @return list<PhabricatorCustomFieldIndexStorage> List of indexes.
    * @task appsearch
    */
   public function buildFieldIndexes() {
     if ($this->proxy) {
       return $this->proxy->buildFieldIndexes();
     }
     return array();
   }
 
 
   /**
    * Return an index against which this field can be meaningfully ordered
    * against to implement ApplicationSearch.
    *
    * This should be a single index, normally built using
    * @{method:newStringIndex} and @{method:newNumericIndex}.
    *
    * The value of the index is not used.
    *
    * Return null from this method if the field can not be ordered.
    *
    * @return PhabricatorCustomFieldIndexStorage A single index to order by.
    * @task appsearch
    */
   public function buildOrderIndex() {
     if ($this->proxy) {
       return $this->proxy->buildOrderIndex();
     }
     return null;
   }
 
 
   /**
    * Build a new empty storage object for storing string indexes. Normally,
    * this should be a concrete subclass of
    * @{class:PhabricatorCustomFieldStringIndexStorage}.
    *
    * @return PhabricatorCustomFieldStringIndexStorage Storage object.
    * @task appsearch
    */
   protected function newStringIndexStorage() {
     // NOTE: This intentionally isn't proxied, to avoid call cycles.
     throw new PhabricatorCustomFieldImplementationIncompleteException($this);
   }
 
 
   /**
    * Build a new empty storage object for storing string indexes. Normally,
    * this should be a concrete subclass of
    * @{class:PhabricatorCustomFieldStringIndexStorage}.
    *
    * @return PhabricatorCustomFieldStringIndexStorage Storage object.
    * @task appsearch
    */
   protected function newNumericIndexStorage() {
     // NOTE: This intentionally isn't proxied, to avoid call cycles.
     throw new PhabricatorCustomFieldImplementationIncompleteException($this);
   }
 
 
   /**
    * Build and populate storage for a string index.
    *
    * @param string String to index.
    * @return PhabricatorCustomFieldStringIndexStorage Populated storage.
    * @task appsearch
    */
   protected function newStringIndex($value) {
     if ($this->proxy) {
       return $this->proxy->newStringIndex();
     }
 
     $key = $this->getFieldIndex();
     return $this->newStringIndexStorage()
       ->setIndexKey($key)
       ->setIndexValue($value);
   }
 
 
   /**
    * Build and populate storage for a numeric index.
    *
    * @param string Numeric value to index.
    * @return PhabricatorCustomFieldNumericIndexStorage Populated storage.
    * @task appsearch
    */
   protected function newNumericIndex($value) {
     if ($this->proxy) {
       return $this->proxy->newNumericIndex();
     }
     $key = $this->getFieldIndex();
     return $this->newNumericIndexStorage()
       ->setIndexKey($key)
       ->setIndexValue($value);
   }
 
 
   /**
    * Read a query value from a request, for storage in a saved query. Normally,
    * this method should, e.g., read a string out of the request.
    *
    * @param PhabricatorApplicationSearchEngine Engine building the query.
    * @param AphrontRequest Request to read from.
    * @return wild
    * @task appsearch
    */
   public function readApplicationSearchValueFromRequest(
     PhabricatorApplicationSearchEngine $engine,
     AphrontRequest $request) {
     if ($this->proxy) {
       return $this->proxy->readApplicationSearchValueFromRequest(
         $engine,
         $request);
     }
     throw new PhabricatorCustomFieldImplementationIncompleteException($this);
   }
 
 
   /**
    * Constrain a query, given a field value. Generally, this method should
    * use `with...()` methods to apply filters or other constraints to the
    * query.
    *
    * @param PhabricatorApplicationSearchEngine Engine executing the query.
    * @param PhabricatorCursorPagedPolicyAwareQuery Query to constrain.
    * @param wild Constraint provided by the user.
    * @return void
    * @task appsearch
    */
   public function applyApplicationSearchConstraintToQuery(
     PhabricatorApplicationSearchEngine $engine,
     PhabricatorCursorPagedPolicyAwareQuery $query,
     $value) {
     if ($this->proxy) {
       return $this->proxy->applyApplicationSearchConstraintToQuery(
         $engine,
         $query,
         $value);
     }
     throw new PhabricatorCustomFieldImplementationIncompleteException($this);
   }
 
 
   /**
    * Append search controls to the interface.
    *
    * @param PhabricatorApplicationSearchEngine Engine constructing the form.
    * @param AphrontFormView The form to update.
    * @param wild Value from the saved query.
    * @return void
    * @task appsearch
    */
   public function appendToApplicationSearchForm(
     PhabricatorApplicationSearchEngine $engine,
     AphrontFormView $form,
     $value) {
     if ($this->proxy) {
       return $this->proxy->appendToApplicationSearchForm(
         $engine,
         $form,
         $value);
     }
     throw new PhabricatorCustomFieldImplementationIncompleteException($this);
   }
 
 
 /* -(  ApplicationTransactions  )-------------------------------------------- */
 
 
   /**
    * Appearing in ApplicationTrasactions allows a field to be edited using
    * standard workflows.
    *
    * @return bool True to appear in ApplicationTransactions.
    * @task appxaction
    */
   public function shouldAppearInApplicationTransactions() {
     if ($this->proxy) {
       return $this->proxy->shouldAppearInApplicationTransactions();
     }
     return false;
   }
 
 
   /**
    * @task appxaction
    */
   public function getApplicationTransactionType() {
     if ($this->proxy) {
       return $this->proxy->getApplicationTransactionType();
     }
     return PhabricatorTransactions::TYPE_CUSTOMFIELD;
   }
 
 
   /**
    * @task appxaction
    */
   public function getApplicationTransactionMetadata() {
     if ($this->proxy) {
       return $this->proxy->getApplicationTransactionMetadata();
     }
     return array();
   }
 
 
   /**
    * @task appxaction
    */
   public function getOldValueForApplicationTransactions() {
     if ($this->proxy) {
       return $this->proxy->getOldValueForApplicationTransactions();
     }
     return $this->getValueForStorage();
   }
 
 
   /**
    * @task appxaction
    */
   public function getNewValueForApplicationTransactions() {
     if ($this->proxy) {
       return $this->proxy->getNewValueForApplicationTransactions();
     }
     return $this->getValueForStorage();
   }
 
 
   /**
    * @task appxaction
    */
   public function setValueFromApplicationTransactions($value) {
     if ($this->proxy) {
       return $this->proxy->setValueFromApplicationTransactions($value);
     }
     return $this->setValueFromStorage($value);
   }
 
 
   /**
    * @task appxaction
    */
   public function getNewValueFromApplicationTransactions(
     PhabricatorApplicationTransaction $xaction) {
     if ($this->proxy) {
       return $this->proxy->getNewValueFromApplicationTransactions($xaction);
     }
     return $xaction->getNewValue();
   }
 
 
   /**
    * @task appxaction
    */
   public function getApplicationTransactionHasEffect(
     PhabricatorApplicationTransaction $xaction) {
     if ($this->proxy) {
       return $this->proxy->getApplicationTransactionHasEffect($xaction);
     }
     return ($xaction->getOldValue() !== $xaction->getNewValue());
   }
 
 
   /**
    * @task appxaction
    */
   public function applyApplicationTransactionInternalEffects(
     PhabricatorApplicationTransaction $xaction) {
     if ($this->proxy) {
       return $this->proxy->applyApplicationTransactionInternalEffects($xaction);
     }
     return;
   }
 
 
   /**
    * @task appxaction
    */
   public function getApplicationTransactionRemarkupBlocks(
     PhabricatorApplicationTransaction $xaction) {
     if ($this->proxy) {
       return $this->proxy->getApplicationTransactionRemarkupBlocks($xaction);
     }
     return array();
   }
 
 
   /**
    * @task appxaction
    */
   public function applyApplicationTransactionExternalEffects(
     PhabricatorApplicationTransaction $xaction) {
     if ($this->proxy) {
       return $this->proxy->applyApplicationTransactionExternalEffects($xaction);
     }
 
     if (!$this->shouldEnableForRole(self::ROLE_STORAGE)) {
       return;
     }
 
     $this->setValueFromApplicationTransactions($xaction->getNewValue());
     $value = $this->getValueForStorage();
 
     $table = $this->newStorageObject();
     $conn_w = $table->establishConnection('w');
 
     if ($value === null) {
       queryfx(
         $conn_w,
         'DELETE FROM %T WHERE objectPHID = %s AND fieldIndex = %s',
         $table->getTableName(),
         $this->getObject()->getPHID(),
         $this->getFieldIndex());
     } else {
       queryfx(
         $conn_w,
         'INSERT INTO %T (objectPHID, fieldIndex, fieldValue)
           VALUES (%s, %s, %s)
           ON DUPLICATE KEY UPDATE fieldValue = VALUES(fieldValue)',
         $table->getTableName(),
         $this->getObject()->getPHID(),
         $this->getFieldIndex(),
         $value);
     }
 
     return;
   }
 
 
   /**
    * Validate transactions for an object. This allows you to raise an error
    * when a transaction would set a field to an invalid value, or when a field
    * is required but no transactions provide value.
    *
    * @param PhabricatorLiskDAO Editor applying the transactions.
    * @param string Transaction type. This type is always
    *   `PhabricatorTransactions::TYPE_CUSTOMFIELD`, it is provided for
    *   convenience when constructing exceptions.
    * @param list<PhabricatorApplicationTransaction> Transactions being applied,
    *   which may be empty if this field is not being edited.
    * @return list<PhabricatorApplicationTransactionValidationError> Validation
    *   errors.
    *
    * @task appxaction
    */
   public function validateApplicationTransactions(
     PhabricatorApplicationTransactionEditor $editor,
     $type,
     array $xactions) {
     if ($this->proxy) {
       return $this->proxy->validateApplicationTransactions(
         $editor,
         $type,
         $xactions);
     }
     return array();
   }
 
   public function getApplicationTransactionTitle(
     PhabricatorApplicationTransaction $xaction) {
     if ($this->proxy) {
       return $this->proxy->getApplicationTransactionTitle(
         $xaction);
     }
 
     $author_phid = $xaction->getAuthorPHID();
     return pht(
       '%s updated this object.',
       $xaction->renderHandleLink($author_phid));
   }
 
   public function getApplicationTransactionTitleForFeed(
     PhabricatorApplicationTransaction $xaction) {
     if ($this->proxy) {
       return $this->proxy->getApplicationTransactionTitleForFeed(
         $xaction);
     }
 
     $author_phid = $xaction->getAuthorPHID();
     $object_phid = $xaction->getObjectPHID();
     return pht(
       '%s updated %s.',
       $xaction->renderHandleLink($author_phid),
       $xaction->renderHandleLink($object_phid));
   }
 
 
   public function getApplicationTransactionHasChangeDetails(
     PhabricatorApplicationTransaction $xaction) {
     if ($this->proxy) {
       return $this->proxy->getApplicationTransactionHasChangeDetails(
         $xaction);
     }
     return false;
   }
 
   public function getApplicationTransactionChangeDetails(
     PhabricatorApplicationTransaction $xaction,
     PhabricatorUser $viewer) {
     if ($this->proxy) {
       return $this->proxy->getApplicationTransactionChangeDetails(
         $xaction,
         $viewer);
     }
     return null;
   }
 
   public function getApplicationTransactionRequiredHandlePHIDs(
     PhabricatorApplicationTransaction $xaction) {
     if ($this->proxy) {
       return $this->proxy->getApplicationTransactionRequiredHandlePHIDs(
         $xaction);
     }
     return array();
   }
 
   public function shouldHideInApplicationTransactions(
     PhabricatorApplicationTransaction $xaction) {
     if ($this->proxy) {
       return $this->proxy->shouldHideInApplicationTransactions($xaction);
     }
     return false;
   }
 
 
 /* -(  Transaction Mail  )--------------------------------------------------- */
 
 
   /**
    * @task xactionmail
    */
   public function shouldAppearInTransactionMail() {
     if ($this->proxy) {
       return $this->proxy->shouldAppearInTransactionMail();
     }
     return false;
   }
 
 
   /**
    * @task xactionmail
    */
   public function updateTransactionMailBody(
     PhabricatorMetaMTAMailBody $body,
     PhabricatorApplicationTransactionEditor $editor,
     array $xactions) {
     if ($this->proxy) {
       return $this->proxy->updateTransactionMailBody($body, $editor, $xactions);
     }
     return;
   }
 
 
 /* -(  Edit View  )---------------------------------------------------------- */
 
 
   /**
    * @task edit
    */
   public function shouldAppearInEditView() {
     if ($this->proxy) {
       return $this->proxy->shouldAppearInEditView();
     }
     return false;
   }
 
 
   /**
    * @task edit
    */
   public function readValueFromRequest(AphrontRequest $request) {
     if ($this->proxy) {
       return $this->proxy->readValueFromRequest($request);
     }
     throw new PhabricatorCustomFieldImplementationIncompleteException($this);
   }
 
 
   /**
    * @task edit
    */
   public function getRequiredHandlePHIDsForEdit() {
     if ($this->proxy) {
       return $this->proxy->getRequiredHandlePHIDsForEdit();
     }
     return array();
   }
 
 
   /**
    * @task edit
    */
   public function getInstructionsForEdit() {
     if ($this->proxy) {
       return $this->proxy->getInstructionsForEdit();
     }
     return null;
   }
 
 
   /**
    * @task edit
    */
   public function renderEditControl(array $handles) {
     if ($this->proxy) {
       return $this->proxy->renderEditControl($handles);
     }
     throw new PhabricatorCustomFieldImplementationIncompleteException($this);
   }
 
 
 /* -(  Property View  )------------------------------------------------------ */
 
 
   /**
    * @task view
    */
   public function shouldAppearInPropertyView() {
     if ($this->proxy) {
       return $this->proxy->shouldAppearInPropertyView();
     }
     return false;
   }
 
 
   /**
    * @task view
    */
   public function renderPropertyViewLabel() {
     if ($this->proxy) {
       return $this->proxy->renderPropertyViewLabel();
     }
     return $this->getFieldName();
   }
 
 
   /**
    * @task view
    */
   public function renderPropertyViewValue(array $handles) {
     if ($this->proxy) {
       return $this->proxy->renderPropertyViewValue($handles);
     }
     throw new PhabricatorCustomFieldImplementationIncompleteException($this);
   }
 
 
   /**
    * @task view
    */
   public function getStyleForPropertyView() {
     if ($this->proxy) {
       return $this->proxy->getStyleForPropertyView();
     }
     return 'property';
   }
 
 
   /**
    * @task view
    */
   public function getIconForPropertyView() {
     if ($this->proxy) {
       return $this->proxy->getIconForPropertyView();
     }
     return null;
   }
 
 
   /**
    * @task view
    */
   public function getRequiredHandlePHIDsForPropertyView() {
     if ($this->proxy) {
       return $this->proxy->getRequiredHandlePHIDsForPropertyView();
     }
     return array();
   }
 
 
 /* -(  List View  )---------------------------------------------------------- */
 
 
   /**
    * @task list
    */
   public function shouldAppearInListView() {
     if ($this->proxy) {
       return $this->proxy->shouldAppearInListView();
     }
     return false;
   }
 
 
   /**
    * @task list
    */
   public function renderOnListItem(PHUIObjectItemView $view) {
     if ($this->proxy) {
       return $this->proxy->renderOnListItem($view);
     }
     throw new PhabricatorCustomFieldImplementationIncompleteException($this);
   }
 
 
 /* -(  Global Search  )------------------------------------------------------ */
 
 
   /**
    * @task globalsearch
    */
   public function shouldAppearInGlobalSearch() {
     if ($this->proxy) {
       return $this->proxy->shouldAppearInGlobalSearch();
     }
     return false;
   }
 
 
   /**
    * @task globalsearch
    */
   public function updateAbstractDocument(
     PhabricatorSearchAbstractDocument $document) {
     if ($this->proxy) {
       return $this->proxy->updateAbstractDocument($document);
     }
     return $document;
   }
 
 
 /* -(  Conduit  )------------------------------------------------------------ */
 
 
   /**
    * @task conduit
    */
   public function shouldAppearInConduitDictionary() {
     if ($this->proxy) {
       return $this->proxy->shouldAppearInConduitDictionary();
     }
     return false;
   }
 
 
   /**
    * @task conduit
    */
   public function getConduitDictionaryValue() {
     if ($this->proxy) {
       return $this->proxy->getConduitDictionaryValue();
     }
     throw new PhabricatorCustomFieldImplementationIncompleteException($this);
   }
 
 
 /* -(  Herald  )------------------------------------------------------------- */
 
 
   /**
    * Return `true` to make this field available in Herald.
    *
    * @return bool True to expose the field in Herald.
    * @task herald
    */
   public function shouldAppearInHerald() {
     if ($this->proxy) {
       return $this->proxy->shouldAppearInHerald();
     }
     return false;
   }
 
 
   /**
    * Get the name of the field in Herald. By default, this uses the
    * normal field name.
    *
    * @return string Herald field name.
    * @task herald
    */
   public function getHeraldFieldName() {
     if ($this->proxy) {
       return $this->proxy->getHeraldFieldName();
     }
     return $this->getFieldName();
   }
 
 
   /**
    * Get the field value for evaluation by Herald.
    *
    * @return wild Field value.
    * @task herald
    */
   public function getHeraldFieldValue() {
     if ($this->proxy) {
       return $this->proxy->getHeraldFieldValue();
     }
     throw new PhabricatorCustomFieldImplementationIncompleteException($this);
   }
 
 
   /**
    * Get the available conditions for this field in Herald.
    *
    * @return list<const> List of Herald condition constants.
    * @task herald
    */
   public function getHeraldFieldConditions() {
     if ($this->proxy) {
       return $this->proxy->getHeraldFieldConditions();
     }
     throw new PhabricatorCustomFieldImplementationIncompleteException($this);
   }
 
 
   /**
    * Get the Herald value type for the given condition.
    *
    * @param   const       Herald condition constant.
    * @return  const|null  Herald value type, or null to use the default.
    * @task herald
    */
   public function getHeraldFieldValueType($condition) {
     if ($this->proxy) {
       return $this->proxy->getHeraldFieldValueType($condition);
     }
     return null;
   }
 
 
 }
diff --git a/src/infrastructure/customfield/field/PhabricatorCustomFieldAttachment.php b/src/infrastructure/customfield/field/PhabricatorCustomFieldAttachment.php
index 068763527..31813c0c3 100644
--- a/src/infrastructure/customfield/field/PhabricatorCustomFieldAttachment.php
+++ b/src/infrastructure/customfield/field/PhabricatorCustomFieldAttachment.php
@@ -1,30 +1,30 @@
 <?php
 
 /**
  * Convenience class which simplifies the implementation of
  * @{interface:PhabricatorCustomFieldInterface} by obscuring the details of how
  * custom fields are stored.
  *
  * Generally, you should not use this class directly. It is used by
  * @{class:PhabricatorCustomField} to manage field storage on objects.
  */
-final class PhabricatorCustomFieldAttachment {
+final class PhabricatorCustomFieldAttachment extends Phobject {
 
   private $lists = array();
 
   public function addCustomFieldList($role, PhabricatorCustomFieldList $list) {
     $this->lists[$role] = $list;
     return $this;
   }
 
   public function getCustomFieldList($role) {
     if (empty($this->lists[$role])) {
       throw new PhabricatorCustomFieldNotAttachedException(
         pht(
           "Role list '%s' is not available!",
           $role));
     }
     return $this->lists[$role];
   }
 
 }
diff --git a/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldCredential.php b/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldCredential.php
index 94fbe165a..d842ef3d1 100644
--- a/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldCredential.php
+++ b/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldCredential.php
@@ -1,138 +1,138 @@
 <?php
 
 final class PhabricatorStandardCustomFieldCredential
   extends PhabricatorStandardCustomField {
 
   public function getFieldType() {
     return 'credential';
   }
 
   public function buildFieldIndexes() {
     $indexes = array();
 
     $value = $this->getFieldValue();
     if (strlen($value)) {
       $indexes[] = $this->newStringIndex($value);
     }
 
     return $indexes;
   }
 
   public function renderEditControl(array $handles) {
     $provides_type = $this->getFieldConfigValue('credential.provides');
     $credential_type = $this->getFieldConfigValue('credential.type');
 
     $all_types = PassphraseCredentialType::getAllProvidesTypes();
     if (!in_array($provides_type, $all_types)) {
-      $provides_type = PassphraseCredentialTypePassword::PROVIDES_TYPE;
+      $provides_type = PassphrasePasswordCredentialType::PROVIDES_TYPE;
     }
 
     $credentials = id(new PassphraseCredentialQuery())
       ->setViewer($this->getViewer())
       ->withIsDestroyed(false)
       ->withProvidesTypes(array($provides_type))
       ->execute();
 
     return id(new PassphraseCredentialControl())
       ->setLabel($this->getFieldName())
       ->setName($this->getFieldKey())
       ->setCaption($this->getCaption())
       ->setAllowNull(!$this->getRequired())
       ->setCredentialType($credential_type)
       ->setValue($this->getFieldValue())
       ->setError($this->getFieldError())
       ->setOptions($credentials);
   }
 
   public function getRequiredHandlePHIDsForPropertyView() {
     $value = $this->getFieldValue();
     if ($value) {
       return array($value);
     }
     return array();
   }
 
   public function renderPropertyViewValue(array $handles) {
     $value = $this->getFieldValue();
     if ($value) {
       return $handles[$value]->renderLink();
     }
     return null;
   }
 
   public function validateApplicationTransactions(
     PhabricatorApplicationTransactionEditor $editor,
     $type,
     array $xactions) {
 
     $errors = parent::validateApplicationTransactions(
       $editor,
       $type,
       $xactions);
 
     $ok = PassphraseCredentialControl::validateTransactions(
       $this->getViewer(),
       $xactions);
 
     if (!$ok) {
       foreach ($xactions as $xaction) {
         $errors[] = new PhabricatorApplicationTransactionValidationError(
           $type,
           pht('Invalid'),
           pht(
             'The selected credential does not exist, or you do not have '.
             'permission to use it.'),
           $xaction);
         $this->setFieldError(pht('Invalid'));
       }
     }
 
     return $errors;
   }
 
   public function getApplicationTransactionRequiredHandlePHIDs(
     PhabricatorApplicationTransaction $xaction) {
     $phids = array();
     $old = $xaction->getOldValue();
     $new = $xaction->getNewValue();
     if ($old) {
       $phids[] = $old;
     }
     if ($new) {
       $phids[] = $new;
     }
     return $phids;
   }
 
 
   public function getApplicationTransactionTitle(
     PhabricatorApplicationTransaction $xaction) {
     $author_phid = $xaction->getAuthorPHID();
 
     $old = $xaction->getOldValue();
     $new = $xaction->getNewValue();
 
     if ($old && !$new) {
       return pht(
         '%s removed %s as %s.',
         $xaction->renderHandleLink($author_phid),
         $xaction->renderHandleLink($old),
         $this->getFieldName());
     } else if ($new && !$old) {
       return pht(
         '%s set %s to %s.',
         $xaction->renderHandleLink($author_phid),
         $this->getFieldName(),
         $xaction->renderHandleLink($new));
     } else {
       return pht(
         '%s changed %s from %s to %s.',
         $xaction->renderHandleLink($author_phid),
         $this->getFieldName(),
         $xaction->renderHandleLink($old),
         $xaction->renderHandleLink($new));
     }
   }
 
 
 }
diff --git a/src/infrastructure/daemon/bot/PhabricatorBotMessage.php b/src/infrastructure/daemon/bot/PhabricatorBotMessage.php
index 27f0c17db..64de22f2c 100644
--- a/src/infrastructure/daemon/bot/PhabricatorBotMessage.php
+++ b/src/infrastructure/daemon/bot/PhabricatorBotMessage.php
@@ -1,52 +1,52 @@
 <?php
 
-final class PhabricatorBotMessage {
+final class PhabricatorBotMessage extends Phobject {
 
   private $sender;
   private $command;
   private $body;
   private $target;
   private $public;
 
   public function __construct() {
     // By default messages are public
     $this->public = true;
   }
 
   public function setSender(PhabricatorBotTarget $sender = null) {
     $this->sender = $sender;
     return $this;
   }
 
   public function getSender() {
     return $this->sender;
   }
 
   public function setCommand($command) {
     $this->command = $command;
     return $this;
   }
 
   public function getCommand() {
     return $this->command;
   }
 
   public function setBody($body) {
     $this->body = $body;
     return $this;
   }
 
   public function getBody() {
     return $this->body;
   }
 
   public function setTarget(PhabricatorBotTarget $target = null) {
     $this->target = $target;
     return $this;
   }
 
   public function getTarget() {
     return $this->target;
   }
 
 }
diff --git a/src/infrastructure/daemon/bot/adapter/PhabricatorProtocolAdapter.php b/src/infrastructure/daemon/bot/adapter/PhabricatorProtocolAdapter.php
index c12c62b44..89283264b 100644
--- a/src/infrastructure/daemon/bot/adapter/PhabricatorProtocolAdapter.php
+++ b/src/infrastructure/daemon/bot/adapter/PhabricatorProtocolAdapter.php
@@ -1,62 +1,62 @@
 <?php
 
 /**
  * Defines the api for protocol adapters for @{class:PhabricatorBot}
  */
-abstract class PhabricatorProtocolAdapter {
+abstract class PhabricatorProtocolAdapter extends Phobject {
 
   private $config;
 
   public function setConfig($config) {
     $this->config = $config;
     return $this;
   }
 
   public function getConfig($key, $default = null) {
     return idx($this->config, $key, $default);
   }
 
   /**
    * Performs any connection logic necessary for the protocol
    */
   abstract public function connect();
 
   /**
    * Disconnect from the service.
    */
   public function disconnect() {
     return;
   }
 
   /**
    * This is the spout for messages coming in from the protocol.
    * This will be called in the main event loop of the bot daemon
    * So if if doesn't implement some sort of blocking timeout
    * (e.g. select-based socket polling), it should at least sleep
    * for some period of time in order to not overwhelm the processor.
    *
    * @param Int $poll_frequency The number of seconds between polls
    */
   abstract public function getNextMessages($poll_frequency);
 
   /**
    * This is the output mechanism for the protocol.
    *
    * @param PhabricatorBotMessage $message The message to write
    */
   abstract public function writeMessage(PhabricatorBotMessage $message);
 
   /**
    * String identifying the service type the adapter provides access to, like
    * "irc", "campfire", "flowdock", "hipchat", etc.
    */
   abstract public function getServiceType();
 
   /**
    * String identifying the service name the adapter is connecting to. This is
    * used to distinguish between instances of a service. For example, for IRC,
    * this should return the IRC network the client is connecting to.
    */
   abstract public function getServiceName();
 
 }
diff --git a/src/infrastructure/daemon/bot/handler/PhabricatorBotHandler.php b/src/infrastructure/daemon/bot/handler/PhabricatorBotHandler.php
index 2b41cb63f..dad7cbd1b 100644
--- a/src/infrastructure/daemon/bot/handler/PhabricatorBotHandler.php
+++ b/src/infrastructure/daemon/bot/handler/PhabricatorBotHandler.php
@@ -1,72 +1,72 @@
 <?php
 
 /**
  * Responds to IRC messages. You plug a bunch of these into a
  * @{class:PhabricatorBot} to give it special behavior.
  */
-abstract class PhabricatorBotHandler {
+abstract class PhabricatorBotHandler extends Phobject {
 
   private $bot;
 
   final public function __construct(PhabricatorBot $irc_bot) {
     $this->bot = $irc_bot;
   }
 
   final protected function writeMessage(PhabricatorBotMessage $message) {
     $this->bot->writeMessage($message);
     return $this;
   }
 
   final protected function getConduit() {
     return $this->bot->getConduit();
   }
 
   final protected function getConfig($key, $default = null) {
     return $this->bot->getConfig($key, $default);
   }
 
   final protected function getURI($path) {
     $base_uri = new PhutilURI($this->bot->getConfig('conduit.uri'));
     $base_uri->setPath($path);
     return (string)$base_uri;
   }
 
   final protected function getServiceName() {
     return $this->bot->getAdapter()->getServiceName();
   }
 
   final protected function getServiceType() {
     return $this->bot->getAdapter()->getServiceType();
   }
 
   abstract public function receiveMessage(PhabricatorBotMessage $message);
 
   public function runBackgroundTasks() {
     return;
   }
 
   public function replyTo(PhabricatorBotMessage $original_message, $body) {
     if ($original_message->getCommand() != 'MESSAGE') {
       throw new Exception(
         pht('Handler is trying to reply to something which is not a message!'));
     }
 
     $reply = id(new PhabricatorBotMessage())
       ->setCommand('MESSAGE');
 
     if ($original_message->getTarget()->isPublic()) {
       // This is a public target, like a chatroom. Send the response to the
       // chatroom.
       $reply->setTarget($original_message->getTarget());
     } else {
       // This is a private target, like a private message. Send the response
       // back to the sender (presumably, we are the target).
       $reply->setTarget($original_message->getSender());
     }
 
     $reply->setBody($body);
 
     return $this->writeMessage($reply);
   }
 
 }
diff --git a/src/infrastructure/daemon/bot/target/PhabricatorBotTarget.php b/src/infrastructure/daemon/bot/target/PhabricatorBotTarget.php
index 80a516eb3..e73069f2c 100644
--- a/src/infrastructure/daemon/bot/target/PhabricatorBotTarget.php
+++ b/src/infrastructure/daemon/bot/target/PhabricatorBotTarget.php
@@ -1,22 +1,22 @@
 <?php
 
 /**
  * Represents something which can be the target of messages, like a user or
  * channel.
  */
-abstract class PhabricatorBotTarget {
+abstract class PhabricatorBotTarget extends Phobject {
 
   private $name;
 
   public function setName($name) {
     $this->name = $name;
     return $this;
   }
 
   public function getName() {
     return $this->name;
   }
 
   abstract public function isPublic();
 
 }
diff --git a/src/infrastructure/daemon/control/PhabricatorDaemonReference.php b/src/infrastructure/daemon/control/PhabricatorDaemonReference.php
index bb09082ad..d9f818093 100644
--- a/src/infrastructure/daemon/control/PhabricatorDaemonReference.php
+++ b/src/infrastructure/daemon/control/PhabricatorDaemonReference.php
@@ -1,162 +1,162 @@
 <?php
 
-final class PhabricatorDaemonReference {
+final class PhabricatorDaemonReference extends Phobject {
 
   private $name;
   private $argv;
   private $pid;
   private $start;
   private $pidFile;
 
   private $daemonLog;
 
   public static function loadReferencesFromFile($path) {
     $pid_data = Filesystem::readFile($path);
 
     try {
       $dict = phutil_json_decode($pid_data);
     } catch (PhutilJSONParserException $ex) {
       $dict = array();
     }
 
     $refs = array();
     $daemons = idx($dict, 'daemons', array());
 
     $logs = array();
 
     $daemon_ids = ipull($daemons, 'id');
     if ($daemon_ids) {
       try {
         $logs = id(new PhabricatorDaemonLogQuery())
           ->setViewer(PhabricatorUser::getOmnipotentUser())
           ->withDaemonIDs($daemon_ids)
           ->execute();
       } catch (AphrontQueryException $ex) {
         // Ignore any issues here; getting this information only allows us
         // to provide a more complete picture of daemon status, and we want
         // these commands to work if the database is inaccessible.
       }
 
       $logs = mpull($logs, null, 'getDaemonID');
     }
 
     // Support PID files that use the old daemon format, where each overseer
     // had exactly one daemon. We can eventually remove this; they will still
     // be stopped by `phd stop --force` even if we don't identify them here.
     if (!$daemons && idx($dict, 'name')) {
       $daemons = array(
         array(
           'config' => array(
             'class' => idx($dict, 'name'),
             'argv' => idx($dict, 'argv', array()),
           ),
         ),
       );
     }
 
     foreach ($daemons as $daemon) {
       $ref = new PhabricatorDaemonReference();
 
       // NOTE: This is the overseer PID, not the actual daemon process PID.
       // This is correct for checking status and sending signals (the only
       // things we do with it), but might be confusing. $daemon['pid'] has
       // the daemon PID, and we could expose that if we had some use for it.
 
       $ref->pid = idx($dict, 'pid');
       $ref->start = idx($dict, 'start');
 
       $config = idx($daemon, 'config', array());
       $ref->name = idx($config, 'class');
       $ref->argv = idx($config, 'argv', array());
 
       $log = idx($logs, idx($daemon, 'id'));
       if ($log) {
         $ref->daemonLog = $log;
       }
 
       $ref->pidFile = $path;
       $refs[] = $ref;
     }
 
     return $refs;
   }
 
   public function updateStatus($new_status) {
     if (!$this->daemonLog) {
       return;
     }
 
     try {
       $this->daemonLog
         ->setStatus($new_status)
         ->save();
     } catch (AphrontQueryException $ex) {
       // Ignore anything that goes wrong here.
     }
   }
 
   public function getPID() {
     return $this->pid;
   }
 
   public function getName() {
     return $this->name;
   }
 
   public function getArgv() {
     return $this->argv;
   }
 
   public function getEpochStarted() {
     return $this->start;
   }
 
   public function getPIDFile() {
     return $this->pidFile;
   }
 
   public function getDaemonLog() {
     return $this->daemonLog;
   }
 
   public function isRunning() {
     return self::isProcessRunning($this->getPID());
   }
 
   public static function isProcessRunning($pid) {
     if (!$pid) {
       return false;
     }
 
     if (function_exists('posix_kill')) {
       // This may fail if we can't signal the process because we are running as
       // a different user (for example, we are 'apache' and the process is some
       // other user's, or we are a normal user and the process is root's), but
       // we can check the error code to figure out if the process exists.
       $is_running = posix_kill($pid, 0);
       if (posix_get_last_error() == 1) {
         // "Operation Not Permitted", indicates that the PID exists. If it
         // doesn't, we'll get an error 3 ("No such process") instead.
         $is_running = true;
       }
     } else {
       // If we don't have the posix extension, just exec.
       list($err) = exec_manual('ps %s', $pid);
       $is_running = ($err == 0);
     }
 
     return $is_running;
   }
 
   public function waitForExit($seconds) {
     $start = time();
     while (time() < $start + $seconds) {
       usleep(100000);
       if (!$this->isRunning()) {
         return true;
       }
     }
     return !$this->isRunning();
   }
 
 }
diff --git a/src/infrastructure/daemon/workers/PhabricatorWorker.php b/src/infrastructure/daemon/workers/PhabricatorWorker.php
index 8992474a2..c64135531 100644
--- a/src/infrastructure/daemon/workers/PhabricatorWorker.php
+++ b/src/infrastructure/daemon/workers/PhabricatorWorker.php
@@ -1,258 +1,258 @@
 <?php
 
 /**
  * @task config   Configuring Retries and Failures
  */
-abstract class PhabricatorWorker {
+abstract class PhabricatorWorker extends Phobject {
 
   private $data;
   private static $runAllTasksInProcess = false;
   private $queuedTasks = array();
 
   // NOTE: Lower priority numbers execute first. The priority numbers have to
   // have the same ordering that IDs do (lowest first) so MySQL can use a
   // multipart key across both of them efficiently.
 
   const PRIORITY_ALERTS  = 1000;
   const PRIORITY_DEFAULT = 2000;
   const PRIORITY_BULK    = 3000;
   const PRIORITY_IMPORT  = 4000;
 
 
 /* -(  Configuring Retries and Failures  )----------------------------------- */
 
 
   /**
    * Return the number of seconds this worker needs hold a lease on the task for
    * while it performs work. For most tasks you can leave this at `null`, which
    * will give you a default lease (currently 2 hours).
    *
    * For tasks which may take a very long time to complete, you should return
    * an upper bound on the amount of time the task may require.
    *
    * @return int|null  Number of seconds this task needs to remain leased for,
    *                   or null for a default lease.
    *
    * @task config
    */
   public function getRequiredLeaseTime() {
     return null;
   }
 
 
   /**
    * Return the maximum number of times this task may be retried before it is
    * considered permanently failed. By default, tasks retry indefinitely. You
    * can throw a @{class:PhabricatorWorkerPermanentFailureException} to cause an
    * immediate permanent failure.
    *
    * @return int|null  Number of times the task will retry before permanent
    *                   failure. Return `null` to retry indefinitely.
    *
    * @task config
    */
   public function getMaximumRetryCount() {
     return null;
   }
 
 
   /**
    * Return the number of seconds a task should wait after a failure before
    * retrying. For most tasks you can leave this at `null`, which will give you
    * a short default retry period (currently 60 seconds).
    *
    * @param  PhabricatorWorkerTask  The task itself. This object is probably
    *                                useful mostly to examine the failure count
    *                                if you want to implement staggered retries,
    *                                or to examine the execution exception if
    *                                you want to react to different failures in
    *                                different ways.
    * @return int|null               Number of seconds to wait between retries,
    *                                or null for a default retry period
    *                                (currently 60 seconds).
    *
    * @task config
    */
   public function getWaitBeforeRetry(PhabricatorWorkerTask $task) {
     return null;
   }
 
   abstract protected function doWork();
 
   final public function __construct($data) {
     $this->data = $data;
   }
 
   final protected function getTaskData() {
     return $this->data;
   }
 
   final public function executeTask() {
     $this->doWork();
   }
 
   final public static function scheduleTask(
     $task_class,
     $data,
     $options = array()) {
 
     $priority = idx($options, 'priority');
     if ($priority === null) {
       $priority = self::PRIORITY_DEFAULT;
     }
     $object_phid = idx($options, 'objectPHID');
 
     $task = id(new PhabricatorWorkerActiveTask())
       ->setTaskClass($task_class)
       ->setData($data)
       ->setPriority($priority)
       ->setObjectPHID($object_phid);
 
     if (self::$runAllTasksInProcess) {
       // Do the work in-process.
       $worker = newv($task_class, array($data));
 
       while (true) {
         try {
           $worker->doWork();
           foreach ($worker->getQueuedTasks() as $queued_task) {
             list($queued_class, $queued_data, $queued_priority) = $queued_task;
             $queued_options = array('priority' => $queued_priority);
             self::scheduleTask($queued_class, $queued_data, $queued_options);
           }
           break;
         } catch (PhabricatorWorkerYieldException $ex) {
           phlog(
             pht(
               'In-process task "%s" yielded for %s seconds, sleeping...',
               $task_class,
               $ex->getDuration()));
           sleep($ex->getDuration());
         }
       }
 
       // Now, save a task row and immediately archive it so we can return an
       // object with a valid ID.
       $task->openTransaction();
         $task->save();
         $archived = $task->archiveTask(
           PhabricatorWorkerArchiveTask::RESULT_SUCCESS,
           0);
       $task->saveTransaction();
 
       return $archived;
     } else {
       $task->save();
       return $task;
     }
   }
 
 
   /**
    * Wait for tasks to complete. If tasks are not leased by other workers, they
    * will be executed in this process while waiting.
    *
    * @param list<int>   List of queued task IDs to wait for.
    * @return void
    */
   final public static function waitForTasks(array $task_ids) {
     if (!$task_ids) {
       return;
     }
 
     $task_table = new PhabricatorWorkerActiveTask();
 
     $waiting = array_fuse($task_ids);
     while ($waiting) {
       $conn_w = $task_table->establishConnection('w');
 
       // Check if any of the tasks we're waiting on are still queued. If they
       // are not, we're done waiting.
       $row = queryfx_one(
         $conn_w,
         'SELECT COUNT(*) N FROM %T WHERE id IN (%Ld)',
         $task_table->getTableName(),
         $waiting);
       if (!$row['N']) {
         // Nothing is queued anymore. Stop waiting.
         break;
       }
 
       $tasks = id(new PhabricatorWorkerLeaseQuery())
         ->withIDs($waiting)
         ->setLimit(1)
         ->execute();
 
       if (!$tasks) {
         // We were not successful in leasing anything. Sleep for a bit and
         // see if we have better luck later.
         sleep(1);
         continue;
       }
 
       $task = head($tasks)->executeTask();
 
       $ex = $task->getExecutionException();
       if ($ex) {
         throw $ex;
       }
     }
 
     $tasks = id(new PhabricatorWorkerArchiveTaskQuery())
       ->withIDs($task_ids);
 
     foreach ($tasks as $task) {
       if ($task->getResult() != PhabricatorWorkerArchiveTask::RESULT_SUCCESS) {
         throw new Exception(pht('Task %d failed!', $task->getID()));
       }
     }
   }
 
   public function renderForDisplay(PhabricatorUser $viewer) {
     $data = PhutilReadableSerializer::printableValue($this->data);
     return phutil_tag('pre', array(), $data);
   }
 
   /**
    * Set this flag to execute scheduled tasks synchronously, in the same
    * process. This is useful for debugging, and otherwise dramatically worse
    * in every way imaginable.
    */
   public static function setRunAllTasksInProcess($all) {
     self::$runAllTasksInProcess = $all;
   }
 
   final protected function log($pattern /* , ... */) {
     $console = PhutilConsole::getConsole();
     $argv = func_get_args();
     call_user_func_array(array($console, 'writeLog'), $argv);
     return $this;
   }
 
 
   /**
    * Queue a task to be executed after this one succeeds.
    *
    * The followup task will be queued only if this task completes cleanly.
    *
    * @param string    Task class to queue.
    * @param array     Data for the followup task.
    * @param int|null  Priority for the followup task.
    * @return this
    */
   final protected function queueTask($class, array $data, $priority = null) {
     $this->queuedTasks[] = array($class, $data, $priority);
     return $this;
   }
 
 
   /**
    * Get tasks queued as followups by @{method:queueTask}.
    *
    * @return list<tuple<string, wild, int|null>> Queued task specifications.
    */
   final public function getQueuedTasks() {
     return $this->queuedTasks;
   }
 
 }
diff --git a/src/infrastructure/diff/PhabricatorDifferenceEngine.php b/src/infrastructure/diff/PhabricatorDifferenceEngine.php
index 6db867722..3b4eb5547 100644
--- a/src/infrastructure/diff/PhabricatorDifferenceEngine.php
+++ b/src/infrastructure/diff/PhabricatorDifferenceEngine.php
@@ -1,171 +1,171 @@
 <?php
 
 /**
  * Utility class which encapsulates some shared behavior between different
  * applications which render diffs.
  *
  * @task config Configuring the Engine
  * @task diff Generating Diffs
  */
-final class PhabricatorDifferenceEngine {
+final class PhabricatorDifferenceEngine extends Phobject {
 
 
   private $ignoreWhitespace;
   private $oldName;
   private $newName;
 
 
 /* -(  Configuring the Engine  )--------------------------------------------- */
 
 
   /**
    * If true, ignore whitespace when computing differences.
    *
    * @param bool Ignore whitespace?
    * @return this
    * @task config
    */
   public function setIgnoreWhitespace($ignore_whitespace) {
     $this->ignoreWhitespace = $ignore_whitespace;
     return $this;
   }
 
 
   /**
    * Set the name to identify the old file with. Primarily cosmetic.
    *
    * @param  string Old file name.
    * @return this
    * @task config
    */
   public function setOldName($old_name) {
     $this->oldName = $old_name;
     return $this;
   }
 
 
   /**
    * Set the name to identify the new file with. Primarily cosmetic.
    *
    * @param  string New file name.
    * @return this
    * @task config
    */
   public function setNewName($new_name) {
     $this->newName = $new_name;
     return $this;
   }
 
 
 /* -(  Generating Diffs  )--------------------------------------------------- */
 
 
   /**
    * Generate a raw diff from two raw files. This is a lower-level API than
    * @{method:generateChangesetFromFileContent}, but may be useful if you need
    * to use a custom parser configuration, as with Diffusion.
    *
    * @param string Entire previous file content.
    * @param string Entire current file content.
    * @return string Raw diff between the two files.
    * @task diff
    */
   public function generateRawDiffFromFileContent($old, $new) {
 
     $options = array();
     if ($this->ignoreWhitespace) {
       $options[] = '-bw';
     }
 
     // Generate diffs with full context.
     $options[] = '-U65535';
 
     $old_name = nonempty($this->oldName, '/dev/universe').' 9999-99-99';
     $new_name = nonempty($this->newName, '/dev/universe').' 9999-99-99';
 
     $options[] = '-L';
     $options[] = $old_name;
     $options[] = '-L';
     $options[] = $new_name;
 
     $old_tmp = new TempFile();
     $new_tmp = new TempFile();
 
     Filesystem::writeFile($old_tmp, $old);
     Filesystem::writeFile($new_tmp, $new);
     list($err, $diff) = exec_manual(
       'diff %Ls %s %s',
       $options,
       $old_tmp,
       $new_tmp);
 
     if (!$err) {
       // This indicates that the two files are the same (or, possibly, the
       // same modulo whitespace differences, which is why we can't do this
       // check trivially before running `diff`). Build a synthetic, changeless
       // diff so that we can still render the raw, unchanged file instead of
       // being forced to just say "this file didn't change" since we don't have
       // the content.
 
       $entire_file = explode("\n", $old);
       foreach ($entire_file as $k => $line) {
         $entire_file[$k] = ' '.$line;
       }
 
       $len = count($entire_file);
       $entire_file = implode("\n", $entire_file);
 
       // TODO: If both files were identical but missing newlines, we probably
       // get this wrong. Unclear if it ever matters.
 
       // This is a bit hacky but the diff parser can handle it.
       $diff = "--- {$old_name}\n".
               "+++ {$new_name}\n".
               "@@ -1,{$len} +1,{$len} @@\n".
               $entire_file."\n";
     } else {
       if ($this->ignoreWhitespace) {
 
         // Under "-bw", `diff` is inconsistent about emitting "\ No newline
         // at end of file". For instance, a long file with a change in the
         // middle will emit a contextless "\ No newline..." at the end if a
         // newline is removed, but not if one is added. A file with a change
         // at the end will emit the "old" "\ No newline..." block only, even
         // if the newline was not removed. Since we're ostensibly ignoring
         // whitespace changes, just drop these lines if they appear anywhere
         // in the diff.
 
         $lines = explode("\n", $diff);
         foreach ($lines as $key => $line) {
           if (isset($line[0]) && $line[0] == '\\') {
             unset($lines[$key]);
           }
         }
         $diff = implode("\n", $lines);
       }
     }
 
     return $diff;
   }
 
 
   /**
    * Generate an @{class:DifferentialChangeset} from two raw files. This is
    * principally useful because you can feed the output to
    * @{class:DifferentialChangesetParser} in order to render it.
    *
    * @param string Entire previous file content.
    * @param string Entire current file content.
    * @return @{class:DifferentialChangeset} Synthetic changeset.
    * @task diff
    */
   public function generateChangesetFromFileContent($old, $new) {
     $diff = $this->generateRawDiffFromFileContent($old, $new);
 
     $changes = id(new ArcanistDiffParser())->parseDiff($diff);
     $diff = DifferentialDiff::newEphemeralFromRawChanges(
       $changes);
     return head($diff->getChangesets());
   }
 
 }
diff --git a/src/infrastructure/edges/constants/PhabricatorEdgeConstants.php b/src/infrastructure/edges/constants/PhabricatorEdgeConstants.php
index a2934d310..53ec853e3 100644
--- a/src/infrastructure/edges/constants/PhabricatorEdgeConstants.php
+++ b/src/infrastructure/edges/constants/PhabricatorEdgeConstants.php
@@ -1,3 +1,3 @@
 <?php
 
-abstract class PhabricatorEdgeConstants {}
+abstract class PhabricatorEdgeConstants extends Phobject {}
diff --git a/src/infrastructure/edges/type/__tests__/PhabricatorEdgeTypeTestCase.php b/src/infrastructure/edges/type/__tests__/PhabricatorEdgeTypeTestCase.php
new file mode 100644
index 000000000..08a9fc126
--- /dev/null
+++ b/src/infrastructure/edges/type/__tests__/PhabricatorEdgeTypeTestCase.php
@@ -0,0 +1,10 @@
+<?php
+
+final class PhabricatorEdgeTypeTestCase extends PhabricatorTestCase {
+
+  public function testGetAllTypes() {
+    PhabricatorEdgeType::getAllTypes();
+    $this->assertTrue(true);
+  }
+
+}
diff --git a/src/infrastructure/env/PhabricatorConfigSource.php b/src/infrastructure/env/PhabricatorConfigSource.php
index 1fbdf2f20..741d94e15 100644
--- a/src/infrastructure/env/PhabricatorConfigSource.php
+++ b/src/infrastructure/env/PhabricatorConfigSource.php
@@ -1,33 +1,33 @@
 <?php
 
-abstract class PhabricatorConfigSource {
+abstract class PhabricatorConfigSource extends Phobject {
 
   private $name;
 
   public function setName($name) {
     $this->name = $name;
     return $this;
   }
 
   public function getName() {
     return $this->name;
   }
 
   abstract public function getKeys(array $keys);
   abstract public function getAllKeys();
 
   public function canWrite() {
     return false;
   }
 
   public function setKeys(array $keys) {
     throw new Exception(
       pht('This configuration source does not support writes.'));
   }
 
   public function deleteKeys(array $keys) {
     throw new Exception(
       pht('This configuration source does not support writes.'));
   }
 
 }
diff --git a/src/infrastructure/env/PhabricatorEnv.php b/src/infrastructure/env/PhabricatorEnv.php
index f6bf920df..d5c4c3446 100644
--- a/src/infrastructure/env/PhabricatorEnv.php
+++ b/src/infrastructure/env/PhabricatorEnv.php
@@ -1,894 +1,894 @@
 <?php
 
 /**
  * Manages the execution environment configuration, exposing APIs to read
  * configuration settings and other similar values that are derived directly
  * from configuration settings.
  *
  *
  * = Reading Configuration =
  *
  * The primary role of this class is to provide an API for reading
  * Phabricator configuration, @{method:getEnvConfig}:
  *
  *   $value = PhabricatorEnv::getEnvConfig('some.key', $default);
  *
  * The class also handles some URI construction based on configuration, via
  * the methods @{method:getURI}, @{method:getProductionURI},
  * @{method:getCDNURI}, and @{method:getDoclink}.
  *
  * For configuration which allows you to choose a class to be responsible for
  * some functionality (e.g., which mail adapter to use to deliver email),
  * @{method:newObjectFromConfig} provides a simple interface that validates
  * the configured value.
  *
  *
  * = Unit Test Support =
  *
  * In unit tests, you can use @{method:beginScopedEnv} to create a temporary,
  * mutable environment. The method returns a scope guard object which restores
  * the environment when it is destroyed. For example:
  *
  *   public function testExample() {
  *     $env = PhabricatorEnv::beginScopedEnv();
  *     $env->overrideEnv('some.key', 'new-value-for-this-test');
  *
  *     // Some test which depends on the value of 'some.key'.
  *
  *   }
  *
  * Your changes will persist until the `$env` object leaves scope or is
  * destroyed.
  *
  * You should //not// use this in normal code.
  *
  *
  * @task read     Reading Configuration
  * @task uri      URI Validation
  * @task test     Unit Test Support
  * @task internal Internals
  */
-final class PhabricatorEnv {
+final class PhabricatorEnv extends Phobject {
 
   private static $sourceStack;
   private static $repairSource;
   private static $overrideSource;
   private static $requestBaseURI;
   private static $cache;
   private static $localeCode;
 
   /**
    * @phutil-external-symbol class PhabricatorStartup
    */
   public static function initializeWebEnvironment() {
     self::initializeCommonEnvironment();
   }
 
   public static function initializeScriptEnvironment() {
     self::initializeCommonEnvironment();
 
     // NOTE: This is dangerous in general, but we know we're in a script context
     // and are not vulnerable to CSRF.
     AphrontWriteGuard::allowDangerousUnguardedWrites(true);
 
     // There are several places where we log information (about errors, events,
     // service calls, etc.) for analysis via DarkConsole or similar. These are
     // useful for web requests, but grow unboundedly in long-running scripts and
     // daemons. Discard data as it arrives in these cases.
     PhutilServiceProfiler::getInstance()->enableDiscardMode();
     DarkConsoleErrorLogPluginAPI::enableDiscardMode();
     DarkConsoleEventPluginAPI::enableDiscardMode();
   }
 
 
   private static function initializeCommonEnvironment() {
     PhutilErrorHandler::initialize();
 
     self::buildConfigurationSourceStack();
 
     // Force a valid timezone. If both PHP and Phabricator configuration are
     // invalid, use UTC.
     $tz = self::getEnvConfig('phabricator.timezone');
     if ($tz) {
       @date_default_timezone_set($tz);
     }
     $ok = @date_default_timezone_set(date_default_timezone_get());
     if (!$ok) {
       date_default_timezone_set('UTC');
     }
 
     // Prepend '/support/bin' and append any paths to $PATH if we need to.
     $env_path = getenv('PATH');
     $phabricator_path = dirname(phutil_get_library_root('phabricator'));
     $support_path = $phabricator_path.'/support/bin';
     $env_path = $support_path.PATH_SEPARATOR.$env_path;
     $append_dirs = self::getEnvConfig('environment.append-paths');
     if (!empty($append_dirs)) {
       $append_path = implode(PATH_SEPARATOR, $append_dirs);
       $env_path = $env_path.PATH_SEPARATOR.$append_path;
     }
     putenv('PATH='.$env_path);
 
     // Write this back into $_ENV, too, so ExecFuture picks it up when creating
     // subprocess environments.
     $_ENV['PATH'] = $env_path;
 
 
     // If an instance identifier is defined, write it into the environment so
     // it's available to subprocesses.
     $instance = self::getEnvConfig('cluster.instance');
     if (strlen($instance)) {
       putenv('PHABRICATOR_INSTANCE='.$instance);
       $_ENV['PHABRICATOR_INSTANCE'] = $instance;
     }
 
     PhabricatorEventEngine::initialize();
 
     // TODO: Add a "locale.default" config option once we have some reasonable
     // defaults which aren't silly nonsense.
     self::setLocaleCode('en_US');
   }
 
   public static function beginScopedLocale($locale_code) {
     return new PhabricatorLocaleScopeGuard($locale_code);
   }
 
   public static function getLocaleCode() {
     return self::$localeCode;
   }
 
   public static function setLocaleCode($locale_code) {
     if (!$locale_code) {
       return;
     }
 
     if ($locale_code == self::$localeCode) {
       return;
     }
 
     try {
       $locale = PhutilLocale::loadLocale($locale_code);
       $translations = PhutilTranslation::getTranslationMapForLocale(
         $locale_code);
 
       $override = self::getEnvConfig('translation.override');
       if (!is_array($override)) {
         $override = array();
       }
 
       PhutilTranslator::getInstance()
         ->setLocale($locale)
         ->setTranslations($override + $translations);
 
       self::$localeCode = $locale_code;
     } catch (Exception $ex) {
       // Just ignore this; the user likely has an out-of-date locale code.
     }
   }
 
   private static function buildConfigurationSourceStack() {
     self::dropConfigCache();
 
     $stack = new PhabricatorConfigStackSource();
     self::$sourceStack = $stack;
 
     $default_source = id(new PhabricatorConfigDefaultSource())
       ->setName(pht('Global Default'));
     $stack->pushSource($default_source);
 
     $env = self::getSelectedEnvironmentName();
     if ($env) {
       $stack->pushSource(
         id(new PhabricatorConfigFileSource($env))
           ->setName(pht("File '%s'", $env)));
     }
 
     $stack->pushSource(
       id(new PhabricatorConfigLocalSource())
         ->setName(pht('Local Config')));
 
     // If the install overrides the database adapter, we might need to load
     // the database adapter class before we can push on the database config.
     // This config is locked and can't be edited from the web UI anyway.
     foreach (self::getEnvConfig('load-libraries') as $library) {
       phutil_load_library($library);
     }
 
     // If custom libraries specify config options, they won't get default
     // values as the Default source has already been loaded, so we get it to
     // pull in all options from non-phabricator libraries now they are loaded.
     $default_source->loadExternalOptions();
 
     // If this install has site config sources, load them now.
     $site_sources = id(new PhutilSymbolLoader())
       ->setAncestorClass('PhabricatorConfigSiteSource')
       ->loadObjects();
     $site_sources = msort($site_sources, 'getPriority');
     foreach ($site_sources as $site_source) {
       $stack->pushSource($site_source);
     }
 
     try {
       $stack->pushSource(
         id(new PhabricatorConfigDatabaseSource('default'))
           ->setName(pht('Database')));
     } catch (AphrontQueryException $exception) {
       // If the database is not available, just skip this configuration
       // source. This happens during `bin/storage upgrade`, `bin/conf` before
       // schema setup, etc.
     }
   }
 
   public static function repairConfig($key, $value) {
     if (!self::$repairSource) {
       self::$repairSource = id(new PhabricatorConfigDictionarySource(array()))
         ->setName(pht('Repaired Config'));
       self::$sourceStack->pushSource(self::$repairSource);
     }
     self::$repairSource->setKeys(array($key => $value));
     self::dropConfigCache();
   }
 
   public static function overrideConfig($key, $value) {
     if (!self::$overrideSource) {
       self::$overrideSource = id(new PhabricatorConfigDictionarySource(array()))
         ->setName(pht('Overridden Config'));
       self::$sourceStack->pushSource(self::$overrideSource);
     }
     self::$overrideSource->setKeys(array($key => $value));
     self::dropConfigCache();
   }
 
   public static function getUnrepairedEnvConfig($key, $default = null) {
     foreach (self::$sourceStack->getStack() as $source) {
       if ($source === self::$repairSource) {
         continue;
       }
       $result = $source->getKeys(array($key));
       if ($result) {
         return $result[$key];
       }
     }
     return $default;
   }
 
   public static function getSelectedEnvironmentName() {
     $env_var = 'PHABRICATOR_ENV';
 
     $env = idx($_SERVER, $env_var);
 
     if (!$env) {
       $env = getenv($env_var);
     }
 
     if (!$env) {
       $env = idx($_ENV, $env_var);
     }
 
     if (!$env) {
       $root = dirname(phutil_get_library_root('phabricator'));
       $path = $root.'/conf/local/ENVIRONMENT';
       if (Filesystem::pathExists($path)) {
         $env = trim(Filesystem::readFile($path));
       }
     }
 
     return $env;
   }
 
   public static function calculateEnvironmentHash() {
     $keys = self::getKeysForConsistencyCheck();
 
     $values = array();
     foreach ($keys as $key) {
       $values[$key] = self::getEnvConfigIfExists($key);
     }
 
     return PhabricatorHash::digest(json_encode($values));
   }
 
   /**
    * Returns a summary of non-default configuration settings to allow the
    * "daemons and web have different config" setup check to list divergent
    * keys.
    */
   public static function calculateEnvironmentInfo() {
     $keys = self::getKeysForConsistencyCheck();
 
     $info = array();
 
     $defaults = id(new PhabricatorConfigDefaultSource())->getAllKeys();
     foreach ($keys as $key) {
       $current = self::getEnvConfigIfExists($key);
       $default = idx($defaults, $key, null);
       if ($current !== $default) {
         $info[$key] = PhabricatorHash::digestForIndex(json_encode($current));
       }
     }
 
     $keys_hash = array_keys($defaults);
     sort($keys_hash);
     $keys_hash = implode("\0", $keys_hash);
     $keys_hash = PhabricatorHash::digestForIndex($keys_hash);
 
     return array(
       'version' => 1,
       'keys' => $keys_hash,
       'values' => $info,
     );
   }
 
 
   /**
    * Compare two environment info summaries to generate a human-readable
    * list of discrepancies.
    */
   public static function compareEnvironmentInfo(array $u, array $v) {
     $issues = array();
 
     $uversion = idx($u, 'version');
     $vversion = idx($v, 'version');
     if ($uversion != $vversion) {
       $issues[] = pht(
         'The two configurations were generated by different versions '.
         'of Phabricator.');
 
       // These may not be comparable, so stop here.
       return $issues;
     }
 
     if ($u['keys'] !== $v['keys']) {
       $issues[] = pht(
         'The two configurations have different keys. This usually means '.
         'that they are running different versions of Phabricator.');
     }
 
     $uval = idx($u, 'values', array());
     $vval = idx($v, 'values', array());
 
     $all_keys = array_keys($uval + $vval);
 
     foreach ($all_keys as $key) {
       $uv = idx($uval, $key);
       $vv = idx($vval, $key);
       if ($uv !== $vv) {
         if ($uv && $vv) {
           $issues[] = pht(
             'The configuration key "%s" is set in both configurations, but '.
             'set to different values.',
             $key);
         } else {
           $issues[] = pht(
             'The configuration key "%s" is set in only one configuration.',
             $key);
         }
       }
     }
 
     return $issues;
   }
 
   private static function getKeysForConsistencyCheck() {
     $keys = array_keys(self::getAllConfigKeys());
     sort($keys);
 
     $skip_keys = self::getEnvConfig('phd.variant-config');
     return array_diff($keys, $skip_keys);
   }
 
 
 /* -(  Reading Configuration  )---------------------------------------------- */
 
 
   /**
    * Get the current configuration setting for a given key.
    *
    * If the key is not found, then throw an Exception.
    *
    * @task read
    */
   public static function getEnvConfig($key) {
     if (isset(self::$cache[$key])) {
       return self::$cache[$key];
     }
 
     if (array_key_exists($key, self::$cache)) {
       return self::$cache[$key];
     }
 
     $result = self::$sourceStack->getKeys(array($key));
     if (array_key_exists($key, $result)) {
       self::$cache[$key] = $result[$key];
       return $result[$key];
     } else {
       throw new Exception(
         pht(
           "No config value specified for key '%s'.",
           $key));
     }
   }
 
 
   /**
    * Get the current configuration setting for a given key. If the key
    * does not exist, return a default value instead of throwing. This is
    * primarily useful for migrations involving keys which are slated for
    * removal.
    *
    * @task read
    */
   public static function getEnvConfigIfExists($key, $default = null) {
     try {
       return self::getEnvConfig($key);
     } catch (Exception $ex) {
       return $default;
     }
   }
 
 
   /**
    * Get the fully-qualified URI for a path.
    *
    * @task read
    */
   public static function getURI($path) {
     return rtrim(self::getAnyBaseURI(), '/').$path;
   }
 
 
   /**
    * Get the fully-qualified production URI for a path.
    *
    * @task read
    */
   public static function getProductionURI($path) {
     // If we're passed a URI which already has a domain, simply return it
     // unmodified. In particular, files may have URIs which point to a CDN
     // domain.
     $uri = new PhutilURI($path);
     if ($uri->getDomain()) {
       return $path;
     }
 
     $production_domain = self::getEnvConfig('phabricator.production-uri');
     if (!$production_domain) {
       $production_domain = self::getAnyBaseURI();
     }
     return rtrim($production_domain, '/').$path;
   }
 
   public static function getAllowedURIs($path) {
     $uri = new PhutilURI($path);
     if ($uri->getDomain()) {
       return $path;
     }
 
     $allowed_uris = self::getEnvConfig('phabricator.allowed-uris');
     $return = array();
     foreach ($allowed_uris as $allowed_uri) {
       $return[] = rtrim($allowed_uri, '/').$path;
     }
 
     return $return;
   }
 
 
   /**
    * Get the fully-qualified production URI for a static resource path.
    *
    * @task read
    */
   public static function getCDNURI($path) {
     $alt = self::getEnvConfig('security.alternate-file-domain');
     if (!$alt) {
       $alt = self::getAnyBaseURI();
     }
     $uri = new PhutilURI($alt);
     $uri->setPath($path);
     return (string)$uri;
   }
 
 
   /**
    * Get the fully-qualified production URI for a documentation resource.
    *
    * @task read
    */
   public static function getDoclink($resource, $type = 'article') {
     $uri = new PhutilURI('https://secure.phabricator.com/diviner/find/');
     $uri->setQueryParam('name', $resource);
     $uri->setQueryParam('type', $type);
     $uri->setQueryParam('jump', true);
     return (string)$uri;
   }
 
 
   /**
    * Build a concrete object from a configuration key.
    *
    * @task read
    */
   public static function newObjectFromConfig($key, $args = array()) {
     $class = self::getEnvConfig($key);
     return newv($class, $args);
   }
 
   public static function getAnyBaseURI() {
     $base_uri = self::getEnvConfig('phabricator.base-uri');
 
     if (!$base_uri) {
       $base_uri = self::getRequestBaseURI();
     }
 
     if (!$base_uri) {
       throw new Exception(
         pht(
           "Define '%s' in your configuration to continue.",
           'phabricator.base-uri'));
     }
 
     return $base_uri;
   }
 
   public static function getRequestBaseURI() {
     return self::$requestBaseURI;
   }
 
   public static function setRequestBaseURI($uri) {
     self::$requestBaseURI = $uri;
   }
 
 /* -(  Unit Test Support  )-------------------------------------------------- */
 
 
   /**
    * @task test
    */
   public static function beginScopedEnv() {
     return new PhabricatorScopedEnv(self::pushTestEnvironment());
   }
 
 
   /**
    * @task test
    */
   private static function pushTestEnvironment() {
     self::dropConfigCache();
     $source = new PhabricatorConfigDictionarySource(array());
     self::$sourceStack->pushSource($source);
     return spl_object_hash($source);
   }
 
 
   /**
    * @task test
    */
   public static function popTestEnvironment($key) {
     self::dropConfigCache();
     $source = self::$sourceStack->popSource();
     $stack_key = spl_object_hash($source);
     if ($stack_key !== $key) {
       self::$sourceStack->pushSource($source);
       throw new Exception(
         pht(
           'Scoped environments were destroyed in a different order than they '.
           'were initialized.'));
     }
   }
 
 
 /* -(  URI Validation  )----------------------------------------------------- */
 
 
   /**
    * Detect if a URI satisfies either @{method:isValidLocalURIForLink} or
    * @{method:isValidRemoteURIForLink}, i.e. is a page on this server or the
    * URI of some other resource which has a valid protocol. This rejects
    * garbage URIs and URIs with protocols which do not appear in the
    * `uri.allowed-protocols` configuration, notably 'javascript:' URIs.
    *
    * NOTE: This method is generally intended to reject URIs which it may be
    * unsafe to put in an "href" link attribute.
    *
    * @param string URI to test.
    * @return bool True if the URI identifies a web resource.
    * @task uri
    */
   public static function isValidURIForLink($uri) {
     return self::isValidLocalURIForLink($uri) ||
            self::isValidRemoteURIForLink($uri);
   }
 
 
   /**
    * Detect if a URI identifies some page on this server.
    *
    * NOTE: This method is generally intended to reject URIs which it may be
    * unsafe to issue a "Location:" redirect to.
    *
    * @param string URI to test.
    * @return bool True if the URI identifies a local page.
    * @task uri
    */
   public static function isValidLocalURIForLink($uri) {
     $uri = (string)$uri;
 
     if (!strlen($uri)) {
       return false;
     }
 
     if (preg_match('/\s/', $uri)) {
       // PHP hasn't been vulnerable to header injection attacks for a bunch of
       // years, but we can safely reject these anyway since they're never valid.
       return false;
     }
 
     // Chrome (at a minimum) interprets backslashes in Location headers and the
     // URL bar as forward slashes. This is probably intended to reduce user
     // error caused by confusion over which key is "forward slash" vs "back
     // slash".
     //
     // However, it means a URI like "/\evil.com" is interpreted like
     // "//evil.com", which is a protocol relative remote URI.
     //
     // Since we currently never generate URIs with backslashes in them, reject
     // these unconditionally rather than trying to figure out how browsers will
     // interpret them.
     if (preg_match('/\\\\/', $uri)) {
       return false;
     }
 
     // Valid URIs must begin with '/', followed by the end of the string or some
     // other non-'/' character. This rejects protocol-relative URIs like
     // "//evil.com/evil_stuff/".
     return (bool)preg_match('@^/([^/]|$)@', $uri);
   }
 
 
   /**
    * Detect if a URI identifies some valid linkable remote resource.
    *
    * @param string URI to test.
    * @return bool True if a URI idenfies a remote resource with an allowed
    *              protocol.
    * @task uri
    */
   public static function isValidRemoteURIForLink($uri) {
     try {
       self::requireValidRemoteURIForLink($uri);
       return true;
     } catch (Exception $ex) {
       return false;
     }
   }
 
 
   /**
    * Detect if a URI identifies a valid linkable remote resource, throwing a
    * detailed message if it does not.
    *
    * A valid linkable remote resource can be safely linked or redirected to.
    * This is primarily a protocol whitelist check.
    *
    * @param string URI to test.
    * @return void
    * @task uri
    */
   public static function requireValidRemoteURIForLink($uri) {
     $uri = new PhutilURI($uri);
 
     $proto = $uri->getProtocol();
     if (!strlen($proto)) {
       throw new Exception(
         pht(
           'URI "%s" is not a valid linkable resource. A valid linkable '.
           'resource URI must specify a protocol.',
           $uri));
     }
 
     $protocols = self::getEnvConfig('uri.allowed-protocols');
     if (!isset($protocols[$proto])) {
       throw new Exception(
         pht(
           'URI "%s" is not a valid linkable resource. A valid linkable '.
           'resource URI must use one of these protocols: %s.',
           $uri,
           implode(', ', array_keys($protocols))));
     }
 
     $domain = $uri->getDomain();
     if (!strlen($domain)) {
       throw new Exception(
         pht(
           'URI "%s" is not a valid linkable resource. A valid linkable '.
           'resource URI must specify a domain.',
           $uri));
     }
   }
 
 
   /**
    * Detect if a URI identifies a valid fetchable remote resource.
    *
    * @param string URI to test.
    * @param list<string> Allowed protocols.
    * @return bool True if the URI is a valid fetchable remote resource.
    * @task uri
    */
   public static function isValidRemoteURIForFetch($uri, array $protocols) {
     try {
       self::requireValidRemoteURIForFetch($uri, $protocols);
       return true;
     } catch (Exception $ex) {
       return false;
     }
   }
 
 
   /**
    * Detect if a URI identifies a valid fetchable remote resource, throwing
    * a detailed message if it does not.
    *
    * A valid fetchable remote resource can be safely fetched using a request
    * originating on this server. This is a primarily an address check against
    * the outbound address blacklist.
    *
    * @param string URI to test.
    * @param list<string> Allowed protocols.
    * @return pair<string, string> Pre-resolved URI and domain.
    * @task uri
    */
   public static function requireValidRemoteURIForFetch(
     $uri,
     array $protocols) {
 
     $uri = new PhutilURI($uri);
 
     $proto = $uri->getProtocol();
     if (!strlen($proto)) {
       throw new Exception(
         pht(
           'URI "%s" is not a valid fetchable resource. A valid fetchable '.
           'resource URI must specify a protocol.',
           $uri));
     }
 
     $protocols = array_fuse($protocols);
     if (!isset($protocols[$proto])) {
       throw new Exception(
         pht(
           'URI "%s" is not a valid fetchable resource. A valid fetchable '.
           'resource URI must use one of these protocols: %s.',
           $uri,
           implode(', ', array_keys($protocols))));
     }
 
     $domain = $uri->getDomain();
     if (!strlen($domain)) {
       throw new Exception(
         pht(
           'URI "%s" is not a valid fetchable resource. A valid fetchable '.
           'resource URI must specify a domain.',
           $uri));
     }
 
     $addresses = gethostbynamel($domain);
     if (!$addresses) {
       throw new Exception(
         pht(
           'URI "%s" is not a valid fetchable resource. The domain "%s" could '.
           'not be resolved.',
           $uri,
           $domain));
     }
 
     foreach ($addresses as $address) {
       if (self::isBlacklistedOutboundAddress($address)) {
         throw new Exception(
           pht(
             'URI "%s" is not a valid fetchable resource. The domain "%s" '.
             'resolves to the address "%s", which is blacklisted for '.
             'outbound requests.',
             $uri,
             $domain,
             $address));
       }
     }
 
     $resolved_uri = clone $uri;
     $resolved_uri->setDomain(head($addresses));
 
     return array($resolved_uri, $domain);
   }
 
 
   /**
    * Determine if an IP address is in the outbound address blacklist.
    *
    * @param string IP address.
    * @return bool True if the address is blacklisted.
    */
   public static function isBlacklistedOutboundAddress($address) {
     $blacklist = self::getEnvConfig('security.outbound-blacklist');
 
     return PhutilCIDRList::newList($blacklist)->containsAddress($address);
   }
 
   public static function isClusterRemoteAddress() {
     $address = idx($_SERVER, 'REMOTE_ADDR');
     if (!$address) {
       throw new Exception(
         pht(
           'Unable to test remote address against cluster whitelist: '.
           'REMOTE_ADDR is not defined.'));
     }
 
     return self::isClusterAddress($address);
   }
 
   public static function isClusterAddress($address) {
     $cluster_addresses = self::getEnvConfig('cluster.addresses');
     if (!$cluster_addresses) {
       throw new Exception(
         pht(
           'Phabricator is not configured to serve cluster requests. '.
           'Set `cluster.addresses` in the configuration to whitelist '.
           'cluster hosts before sending requests that use a cluster '.
           'authentication mechanism.'));
     }
 
     return PhutilCIDRList::newList($cluster_addresses)
       ->containsAddress($address);
   }
 
 /* -(  Internals  )---------------------------------------------------------- */
 
 
   /**
    * @task internal
    */
   public static function envConfigExists($key) {
     return array_key_exists($key, self::$sourceStack->getKeys(array($key)));
   }
 
 
   /**
    * @task internal
    */
   public static function getAllConfigKeys() {
     return self::$sourceStack->getAllKeys();
   }
 
   public static function getConfigSourceStack() {
     return self::$sourceStack;
   }
 
   /**
    * @task internal
    */
   public static function overrideTestEnvConfig($stack_key, $key, $value) {
     $tmp = array();
 
     // If we don't have the right key, we'll throw when popping the last
     // source off the stack.
     do {
       $source = self::$sourceStack->popSource();
       array_unshift($tmp, $source);
       if (spl_object_hash($source) == $stack_key) {
         $source->setKeys(array($key => $value));
         break;
       }
     } while (true);
 
     foreach ($tmp as $source) {
       self::$sourceStack->pushSource($source);
     }
 
     self::dropConfigCache();
   }
 
   private static function dropConfigCache() {
     self::$cache = array();
   }
 
 }
diff --git a/src/infrastructure/env/PhabricatorScopedEnv.php b/src/infrastructure/env/PhabricatorScopedEnv.php
index 86bbe6e6a..3bec720ae 100644
--- a/src/infrastructure/env/PhabricatorScopedEnv.php
+++ b/src/infrastructure/env/PhabricatorScopedEnv.php
@@ -1,59 +1,59 @@
 <?php
 
 /**
  * Scope guard to hold a temporary environment. See @{class:PhabricatorEnv} for
  * instructions on use.
  *
  * @task internal Internals
  * @task override Overriding Environment Configuration
  */
-final class PhabricatorScopedEnv {
+final class PhabricatorScopedEnv extends Phobject {
 
   private $key;
   private $isPopped = false;
 
 /* -(  Overriding Environment Configuration  )------------------------------- */
 
   /**
    * Override a configuration key in this scope, setting it to a new value.
    *
    * @param  string Key to override.
    * @param  wild   New value.
    * @return this
    *
    * @task override
    */
   public function overrideEnvConfig($key, $value) {
     PhabricatorEnv::overrideTestEnvConfig(
       $this->key,
       $key,
       $value);
     return $this;
   }
 
 
 /* -(  Internals  )---------------------------------------------------------- */
 
 
   /**
    * @task internal
    */
   public function __construct($stack_key) {
     $this->key = $stack_key;
   }
 
 
   /**
    * Release the scoped environment.
    *
    * @return void
    * @task internal
    */
   public function __destruct() {
     if (!$this->isPopped) {
       PhabricatorEnv::popTestEnvironment($this->key);
       $this->isPopped = true;
     }
   }
 
 }
diff --git a/src/infrastructure/events/PhabricatorEventEngine.php b/src/infrastructure/events/PhabricatorEventEngine.php
index 0cbeb478a..9206be89b 100644
--- a/src/infrastructure/events/PhabricatorEventEngine.php
+++ b/src/infrastructure/events/PhabricatorEventEngine.php
@@ -1,49 +1,49 @@
 <?php
 
-final class PhabricatorEventEngine {
+final class PhabricatorEventEngine extends Phobject {
 
   public static function initialize() {
     // NOTE: If any of this fails, we just log it and move on. It's important
     // to try to make it through here because users may have difficulty fixing
     // fix the errors if we don't: for example, if we fatal here a user may not
     // be able to run `bin/config` in order to remove an invalid listener.
 
     // Load automatic listeners.
     $listeners = id(new PhutilSymbolLoader())
       ->setAncestorClass('PhabricatorAutoEventListener')
       ->loadObjects();
 
     // Load configured listeners.
     $config_listeners = PhabricatorEnv::getEnvConfig('events.listeners');
     foreach ($config_listeners as $listener_class) {
       try {
         $listeners[] = newv($listener_class, array());
       } catch (Exception $ex) {
         phlog($ex);
       }
     }
 
     // Add built-in listeners.
     $listeners[] = new DarkConsoleEventPluginAPI();
 
     // Add application listeners.
     $applications = PhabricatorApplication::getAllInstalledApplications();
     foreach ($applications as $application) {
       $app_listeners = $application->getEventListeners();
       foreach ($app_listeners as $listener) {
         $listener->setApplication($application);
         $listeners[] = $listener;
       }
     }
 
     // Now, register all of the listeners.
     foreach ($listeners as $listener) {
       try {
         $listener->register();
       } catch (Exception $ex) {
         phlog($ex);
       }
     }
   }
 
 }
diff --git a/src/infrastructure/javelin/Javelin.php b/src/infrastructure/javelin/Javelin.php
index 0e8c86fc6..1e983cfcf 100644
--- a/src/infrastructure/javelin/Javelin.php
+++ b/src/infrastructure/javelin/Javelin.php
@@ -1,15 +1,15 @@
 <?php
 
-final class Javelin {
+final class Javelin extends Phobject {
 
   public static function initBehavior(
     $behavior,
     array $config = array(),
     $source_name = 'phabricator') {
 
     $response = CelerityAPI::getStaticResourceResponse();
 
     $response->initBehavior($behavior, $config, $source_name);
   }
 
 }
diff --git a/src/infrastructure/lint/linter/PhabricatorJavelinLinter.php b/src/infrastructure/lint/linter/PhabricatorJavelinLinter.php
index d235dc889..ac2957a4a 100644
--- a/src/infrastructure/lint/linter/PhabricatorJavelinLinter.php
+++ b/src/infrastructure/lint/linter/PhabricatorJavelinLinter.php
@@ -1,290 +1,290 @@
 <?php
 
 final class PhabricatorJavelinLinter extends ArcanistLinter {
 
   private $symbols = array();
 
   private $symbolsBinary;
   private $haveWarnedAboutBinary;
 
   const LINT_PRIVATE_ACCESS = 1;
   const LINT_MISSING_DEPENDENCY = 2;
   const LINT_UNNECESSARY_DEPENDENCY = 3;
   const LINT_UNKNOWN_DEPENDENCY = 4;
   const LINT_MISSING_BINARY = 5;
 
   public function getInfoName() {
     return pht('Javelin Linter');
   }
 
   public function getInfoDescription() {
     return pht(
       'This linter is intended for use with the Javelin JS library and '.
-      'extensions. Use `javelinsymbols` to run Javelin rules on Javascript '.
-      'source files.');
+      'extensions. Use `%s` to run Javelin rules on Javascript source files.',
+      'javelinsymbols');
   }
 
   private function getBinaryPath() {
     if ($this->symbolsBinary === null) {
       list($err, $stdout) = exec_manual('which javelinsymbols');
       $this->symbolsBinary = ($err ? false : rtrim($stdout));
     }
     return $this->symbolsBinary;
   }
 
   public function willLintPaths(array $paths) {
     if (!$this->getBinaryPath()) {
       return;
     }
 
     $root = dirname(phutil_get_library_root('phabricator'));
     require_once $root.'/scripts/__init_script__.php';
 
     $futures = array();
     foreach ($paths as $path) {
       if ($this->shouldIgnorePath($path)) {
         continue;
       }
 
       $future = $this->newSymbolsFuture($path);
       $futures[$path] = $future;
     }
 
     foreach (id(new FutureIterator($futures))->limit(8) as $path => $future) {
       $this->symbols[$path] = $future->resolvex();
     }
   }
 
   public function getLinterName() {
     return 'JAVELIN';
   }
 
   public function getLinterConfigurationName() {
     return 'javelin';
   }
 
   public function getLintSeverityMap() {
     return array(
       self::LINT_MISSING_BINARY => ArcanistLintSeverity::SEVERITY_WARNING,
     );
   }
 
   public function getLintNameMap() {
     return array(
       self::LINT_PRIVATE_ACCESS =>
         pht('Private Method/Member Access'),
       self::LINT_MISSING_DEPENDENCY =>
         pht('Missing Javelin Dependency'),
       self::LINT_UNNECESSARY_DEPENDENCY =>
         pht('Unnecessary Javelin Dependency'),
       self::LINT_UNKNOWN_DEPENDENCY =>
         pht('Unknown Javelin Dependency'),
       self::LINT_MISSING_BINARY =>
         pht('`%s` Not In Path', 'javelinsymbols'),
     );
   }
 
   public function getCacheGranularity() {
     return ArcanistLinter::GRANULARITY_REPOSITORY;
   }
 
   public function getCacheVersion() {
     $version = '0';
     $binary_path = $this->getBinaryPath();
     if ($binary_path) {
       $version .= '-'.md5_file($binary_path);
     }
     return $version;
   }
 
   private function shouldIgnorePath($path) {
     return preg_match('@/__tests__/|externals/javelin/docs/@', $path);
   }
 
   public function lintPath($path) {
     if ($this->shouldIgnorePath($path)) {
       return;
     }
 
     if (!$this->symbolsBinary) {
       if (!$this->haveWarnedAboutBinary) {
         $this->haveWarnedAboutBinary = true;
         // TODO: Write build documentation for the Javelin binaries and point
         // the user at it.
         $this->raiseLintAtLine(
           1,
           0,
           self::LINT_MISSING_BINARY,
           pht(
             "The '%s' binary in the Javelin project is not available in %s, ".
             "so the Javelin linter can't run. This isn't a big concern, ".
             "but means some Javelin problems can't be automatically detected.",
             'javelinsymbols',
             '$PATH'));
       }
       return;
     }
 
     list($uses, $installs) = $this->getUsedAndInstalledSymbolsForPath($path);
     foreach ($uses as $symbol => $line) {
       $parts = explode('.', $symbol);
       foreach ($parts as $part) {
         if ($part[0] == '_' && $part[1] != '_') {
           $base = implode('.', array_slice($parts, 0, 2));
           if (!array_key_exists($base, $installs)) {
             $this->raiseLintAtLine(
               $line,
               0,
               self::LINT_PRIVATE_ACCESS,
               pht(
                 "This file accesses private symbol '%s' across file ".
                 "boundaries. You may only access private members and methods ".
                 "from the file where they are defined.",
                 $symbol));
           }
           break;
         }
       }
     }
 
     $external_classes = array();
     foreach ($uses as $symbol => $line) {
       $parts = explode('.', $symbol);
       $class = implode('.', array_slice($parts, 0, 2));
       if (!array_key_exists($class, $external_classes) &&
           !array_key_exists($class, $installs)) {
         $external_classes[$class] = $line;
       }
     }
 
     $celerity = CelerityResourceMap::getNamedInstance('phabricator');
 
     $path = preg_replace(
       '@^externals/javelinjs/src/@',
       'webroot/rsrc/js/javelin/',
       $path);
     $need = $external_classes;
 
     $resource_name = substr($path, strlen('webroot/'));
     $requires = $celerity->getRequiredSymbolsForName($resource_name);
     if (!$requires) {
       $requires = array();
     }
 
     foreach ($requires as $key => $requires_symbol) {
       $requires_name = $celerity->getResourceNameForSymbol($requires_symbol);
       if ($requires_name === null) {
         $this->raiseLintAtLine(
           0,
           0,
           self::LINT_UNKNOWN_DEPENDENCY,
           pht(
             "This file %s component '%s', but it does not exist. ".
             "You may need to rebuild the Celerity map.",
             '@requires',
             $requires_symbol));
         unset($requires[$key]);
         continue;
       }
 
       if (preg_match('/\\.css$/', $requires_name)) {
         // If JS requires CSS, just assume everything is fine.
         unset($requires[$key]);
       } else {
         $symbol_path = 'webroot/'.$requires_name;
         list($ignored, $req_install) = $this->getUsedAndInstalledSymbolsForPath(
           $symbol_path);
         if (array_intersect_key($req_install, $external_classes)) {
           $need = array_diff_key($need, $req_install);
           unset($requires[$key]);
         }
       }
     }
 
     foreach ($need as $class => $line) {
       $this->raiseLintAtLine(
         $line,
         0,
         self::LINT_MISSING_DEPENDENCY,
         pht(
           "This file uses '%s' but does not @requires the component ".
           "which installs it. You may need to rebuild the Celerity map.",
           $class));
     }
 
     foreach ($requires as $component) {
       $this->raiseLintAtLine(
         0,
         0,
         self::LINT_UNNECESSARY_DEPENDENCY,
         pht(
           "This file %s component '%s' but does not use anything it provides.",
           '@requires',
           $component));
     }
   }
 
   private function loadSymbols($path) {
     if (empty($this->symbols[$path])) {
       $this->symbols[$path] = $this->newSymbolsFuture($path)->resolvex();
     }
     return $this->symbols[$path];
   }
 
   private function newSymbolsFuture($path) {
     $future = new ExecFuture('javelinsymbols # %s', $path);
     $future->write($this->getData($path));
     return $future;
   }
 
   private function getUsedAndInstalledSymbolsForPath($path) {
     list($symbols) = $this->loadSymbols($path);
     $symbols = trim($symbols);
 
     $uses = array();
     $installs = array();
     if (empty($symbols)) {
       // This file has no symbols.
       return array($uses, $installs);
     }
 
     $symbols = explode("\n", trim($symbols));
     foreach ($symbols as $line) {
       $matches = null;
       if (!preg_match('/^([?+\*])([^:]*):(\d+)$/', $line, $matches)) {
         throw new Exception(
           pht('Received malformed output from `%s`.', 'javelinsymbols'));
       }
       $type = $matches[1];
       $symbol = $matches[2];
       $line = $matches[3];
 
       switch ($type) {
         case '?':
           $uses[$symbol] = $line;
           break;
         case '+':
           $installs['JX.'.$symbol] = $line;
           break;
       }
     }
 
     $contents = $this->getData($path);
 
     $matches = null;
     $count = preg_match_all(
       '/@javelin-installs\W+(\S+)/',
       $contents,
       $matches,
       PREG_PATTERN_ORDER);
 
     if ($count) {
       foreach ($matches[1] as $symbol) {
         $installs[$symbol] = 0;
       }
     }
 
     return array($uses, $installs);
   }
 
 }
diff --git a/src/infrastructure/log/PhabricatorAccessLog.php b/src/infrastructure/log/PhabricatorAccessLog.php
index 5b6d37d74..d3d9979aa 100644
--- a/src/infrastructure/log/PhabricatorAccessLog.php
+++ b/src/infrastructure/log/PhabricatorAccessLog.php
@@ -1,41 +1,41 @@
 <?php
 
-final class PhabricatorAccessLog {
+final class PhabricatorAccessLog extends Phobject {
 
   private static $log;
 
   public static function init() {
     // NOTE: This currently has no effect, but some day we may reuse PHP
     // interpreters to run multiple requests. If we do, it has the effect of
     // throwing away the old log.
     self::$log = null;
   }
 
   public static function getLog() {
     if (!self::$log) {
       $path = PhabricatorEnv::getEnvConfig('log.access.path');
       $format = PhabricatorEnv::getEnvConfig('log.access.format');
       $format = nonempty(
         $format,
         "[%D]\t%p\t%h\t%r\t%u\t%C\t%m\t%U\t%R\t%c\t%T");
 
       // NOTE: Path may be null. We still create the log, it just won't write
       // anywhere.
 
       $log = id(new PhutilDeferredLog($path, $format))
         ->setFailQuietly(true)
         ->setData(
           array(
             'D' => date('r'),
             'h' => php_uname('n'),
             'p' => getmypid(),
             'e' => time(),
           ));
 
       self::$log = $log;
     }
 
     return self::$log;
   }
 
 }
diff --git a/src/infrastructure/markup/PhabricatorMarkupEngine.php b/src/infrastructure/markup/PhabricatorMarkupEngine.php
index 7083a6973..1694d42db 100644
--- a/src/infrastructure/markup/PhabricatorMarkupEngine.php
+++ b/src/infrastructure/markup/PhabricatorMarkupEngine.php
@@ -1,638 +1,639 @@
 <?php
 
 /**
  * Manages markup engine selection, configuration, application, caching and
  * pipelining.
  *
  * @{class:PhabricatorMarkupEngine} can be used to render objects which
  * implement @{interface:PhabricatorMarkupInterface} in a batched, cache-aware
  * way. For example, if you have a list of comments written in remarkup (and
  * the objects implement the correct interface) you can render them by first
  * building an engine and adding the fields with @{method:addObject}.
  *
  *   $field  = 'field:body'; // Field you want to render. Each object exposes
  *                           // one or more fields of markup.
  *
  *   $engine = new PhabricatorMarkupEngine();
  *   foreach ($comments as $comment) {
  *     $engine->addObject($comment, $field);
  *   }
  *
  * Now, call @{method:process} to perform the actual cache/rendering
  * step. This is a heavyweight call which does batched data access and
  * transforms the markup into output.
  *
  *   $engine->process();
  *
  * Finally, do something with the results:
  *
  *   $results = array();
  *   foreach ($comments as $comment) {
  *     $results[] = $engine->getOutput($comment, $field);
  *   }
  *
  * If you have a single object to render, you can use the convenience method
  * @{method:renderOneObject}.
  *
  * @task markup Markup Pipeline
  * @task engine Engine Construction
  */
-final class PhabricatorMarkupEngine {
+final class PhabricatorMarkupEngine extends Phobject {
 
   private $objects = array();
   private $viewer;
   private $contextObject;
   private $version = 15;
+  private $engineCaches = array();
 
 
 /* -(  Markup Pipeline  )---------------------------------------------------- */
 
 
   /**
    * Convenience method for pushing a single object through the markup
    * pipeline.
    *
    * @param PhabricatorMarkupInterface  The object to render.
    * @param string                      The field to render.
    * @param PhabricatorUser             User viewing the markup.
    * @param object                      A context object for policy checks
    * @return string                     Marked up output.
    * @task markup
    */
   public static function renderOneObject(
     PhabricatorMarkupInterface $object,
     $field,
     PhabricatorUser $viewer,
     $context_object = null) {
     return id(new PhabricatorMarkupEngine())
       ->setViewer($viewer)
       ->setContextObject($context_object)
       ->addObject($object, $field)
       ->process()
       ->getOutput($object, $field);
   }
 
 
   /**
    * Queue an object for markup generation when @{method:process} is
    * called. You can retrieve the output later with @{method:getOutput}.
    *
    * @param PhabricatorMarkupInterface  The object to render.
    * @param string                      The field to render.
    * @return this
    * @task markup
    */
   public function addObject(PhabricatorMarkupInterface $object, $field) {
     $key = $this->getMarkupFieldKey($object, $field);
     $this->objects[$key] = array(
       'object' => $object,
       'field'  => $field,
     );
 
     return $this;
   }
 
 
   /**
    * Process objects queued with @{method:addObject}. You can then retrieve
    * the output with @{method:getOutput}.
    *
    * @return this
    * @task markup
    */
   public function process() {
     $keys = array();
     foreach ($this->objects as $key => $info) {
       if (!isset($info['markup'])) {
         $keys[] = $key;
       }
     }
 
     if (!$keys) {
       return;
     }
 
     $objects = array_select_keys($this->objects, $keys);
 
     // Build all the markup engines. We need an engine for each field whether
     // we have a cache or not, since we still need to postprocess the cache.
     $engines = array();
     foreach ($objects as $key => $info) {
       $engines[$key] = $info['object']->newMarkupEngine($info['field']);
       $engines[$key]->setConfig('viewer', $this->viewer);
       $engines[$key]->setConfig('contextObject', $this->contextObject);
     }
 
     // Load or build the preprocessor caches.
     $blocks = $this->loadPreprocessorCaches($engines, $objects);
     $blocks = mpull($blocks, 'getCacheData');
 
     $this->engineCaches = $blocks;
 
     // Finalize the output.
     foreach ($objects as $key => $info) {
       $engine = $engines[$key];
       $field = $info['field'];
       $object = $info['object'];
 
       $output = $engine->postprocessText($blocks[$key]);
       $output = $object->didMarkupText($field, $output, $engine);
       $this->objects[$key]['output'] = $output;
     }
 
     return $this;
   }
 
 
   /**
    * Get the output of markup processing for a field queued with
    * @{method:addObject}. Before you can call this method, you must call
    * @{method:process}.
    *
    * @param PhabricatorMarkupInterface  The object to retrieve.
    * @param string                      The field to retrieve.
    * @return string                     Processed output.
    * @task markup
    */
   public function getOutput(PhabricatorMarkupInterface $object, $field) {
     $key = $this->getMarkupFieldKey($object, $field);
     $this->requireKeyProcessed($key);
 
     return $this->objects[$key]['output'];
   }
 
 
   /**
    * Retrieve engine metadata for a given field.
    *
    * @param PhabricatorMarkupInterface  The object to retrieve.
    * @param string                      The field to retrieve.
    * @param string                      The engine metadata field to retrieve.
    * @param wild                        Optional default value.
    * @task markup
    */
   public function getEngineMetadata(
     PhabricatorMarkupInterface $object,
     $field,
     $metadata_key,
     $default = null) {
 
     $key = $this->getMarkupFieldKey($object, $field);
     $this->requireKeyProcessed($key);
 
     return idx($this->engineCaches[$key]['metadata'], $metadata_key, $default);
   }
 
 
   /**
    * @task markup
    */
   private function requireKeyProcessed($key) {
     if (empty($this->objects[$key])) {
       throw new Exception(
         pht(
           "Call %s before using results (key = '%s').",
           'addObject()',
           $key));
     }
 
     if (!isset($this->objects[$key]['output'])) {
       throw new Exception(
         pht(
           'Call %s before using results.',
           'process()'));
     }
   }
 
 
   /**
    * @task markup
    */
   private function getMarkupFieldKey(
     PhabricatorMarkupInterface $object,
     $field) {
 
     static $custom;
     if ($custom === null) {
       $custom = array_merge(
         self::loadCustomInlineRules(),
         self::loadCustomBlockRules());
 
       $custom = mpull($custom, 'getRuleVersion', null);
       ksort($custom);
       $custom = PhabricatorHash::digestForIndex(serialize($custom));
     }
 
     return $object->getMarkupFieldKey($field).'@'.$this->version.'@'.$custom;
   }
 
 
   /**
    * @task markup
    */
   private function loadPreprocessorCaches(array $engines, array $objects) {
     $blocks = array();
 
     $use_cache = array();
     foreach ($objects as $key => $info) {
       if ($info['object']->shouldUseMarkupCache($info['field'])) {
         $use_cache[$key] = true;
       }
     }
 
     if ($use_cache) {
       try {
         $blocks = id(new PhabricatorMarkupCache())->loadAllWhere(
           'cacheKey IN (%Ls)',
           array_keys($use_cache));
         $blocks = mpull($blocks, null, 'getCacheKey');
       } catch (Exception $ex) {
         phlog($ex);
       }
     }
 
     foreach ($objects as $key => $info) {
       // False check in case MySQL doesn't support unicode characters
       // in the string (T1191), resulting in unserialize returning false.
       if (isset($blocks[$key]) && $blocks[$key]->getCacheData() !== false) {
         // If we already have a preprocessing cache, we don't need to rebuild
         // it.
         continue;
       }
 
       $text = $info['object']->getMarkupText($info['field']);
       $data = $engines[$key]->preprocessText($text);
 
       // NOTE: This is just debugging information to help sort out cache issues.
       // If one machine is misconfigured and poisoning caches you can use this
       // field to hunt it down.
 
       $metadata = array(
         'host' => php_uname('n'),
       );
 
       $blocks[$key] = id(new PhabricatorMarkupCache())
         ->setCacheKey($key)
         ->setCacheData($data)
         ->setMetadata($metadata);
 
       if (isset($use_cache[$key])) {
         // This is just filling a cache and always safe, even on a read pathway.
         $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
           $blocks[$key]->replace();
         unset($unguarded);
       }
     }
 
     return $blocks;
   }
 
 
   /**
    * Set the viewing user. Used to implement object permissions.
    *
    * @param PhabricatorUser The viewing user.
    * @return this
    * @task markup
    */
   public function setViewer(PhabricatorUser $viewer) {
     $this->viewer = $viewer;
     return $this;
   }
 
   /**
    * Set the context object. Used to implement object permissions.
    *
    * @param The object in which context this remarkup is used.
    * @return this
    * @task markup
    */
   public function setContextObject($object) {
     $this->contextObject = $object;
     return $this;
   }
 
 
 /* -(  Engine Construction  )------------------------------------------------ */
 
 
 
   /**
    * @task engine
    */
   public static function newManiphestMarkupEngine() {
     return self::newMarkupEngine(array(
     ));
   }
 
 
   /**
    * @task engine
    */
   public static function newPhrictionMarkupEngine() {
     return self::newMarkupEngine(array(
       'header.generate-toc' => true,
     ));
   }
 
 
   /**
    * @task engine
    */
   public static function newPhameMarkupEngine() {
     return self::newMarkupEngine(array(
       'macros' => false,
       'uri.full' => true,
     ));
   }
 
 
   /**
    * @task engine
    */
   public static function newFeedMarkupEngine() {
     return self::newMarkupEngine(
       array(
         'macros'      => false,
         'youtube'     => false,
       ));
   }
 
   /**
    * @task engine
    */
   public static function newCalendarMarkupEngine() {
     return self::newMarkupEngine(array(
     ));
   }
 
 
   /**
    * @task engine
    */
   public static function newDifferentialMarkupEngine(array $options = array()) {
     return self::newMarkupEngine(array(
       'differential.diff' => idx($options, 'differential.diff'),
     ));
   }
 
 
   /**
    * @task engine
    */
   public static function newDiffusionMarkupEngine(array $options = array()) {
     return self::newMarkupEngine(array(
       'header.generate-toc' => true,
     ));
   }
 
   /**
    * @task engine
    */
   public static function getEngine($ruleset = 'default') {
     static $engines = array();
     if (isset($engines[$ruleset])) {
       return $engines[$ruleset];
     }
 
     $engine = null;
     switch ($ruleset) {
       case 'default':
         $engine = self::newMarkupEngine(array());
         break;
       case 'nolinebreaks':
         $engine = self::newMarkupEngine(array());
         $engine->setConfig('preserve-linebreaks', false);
         break;
       case 'diffusion-readme':
         $engine = self::newMarkupEngine(array());
         $engine->setConfig('preserve-linebreaks', false);
         $engine->setConfig('header.generate-toc', true);
         break;
       case 'diviner':
         $engine = self::newMarkupEngine(array());
         $engine->setConfig('preserve-linebreaks', false);
   //    $engine->setConfig('diviner.renderer', new DivinerDefaultRenderer());
         $engine->setConfig('header.generate-toc', true);
         break;
       case 'extract':
         // Engine used for reference/edge extraction. Turn off anything which
         // is slow and doesn't change reference extraction.
         $engine = self::newMarkupEngine(array());
         $engine->setConfig('pygments.enabled', false);
         break;
       default:
         throw new Exception(pht('Unknown engine ruleset: %s!', $ruleset));
     }
 
     $engines[$ruleset] = $engine;
     return $engine;
   }
 
   /**
    * @task engine
    */
   private static function getMarkupEngineDefaultConfiguration() {
     return array(
       'pygments'      => PhabricatorEnv::getEnvConfig('pygments.enabled'),
       'youtube'       => PhabricatorEnv::getEnvConfig(
         'remarkup.enable-embedded-youtube'),
       'differential.diff' => null,
       'header.generate-toc' => false,
       'macros'        => true,
       'uri.allowed-protocols' => PhabricatorEnv::getEnvConfig(
         'uri.allowed-protocols'),
       'uri.full' => false,
       'syntax-highlighter.engine' => PhabricatorEnv::getEnvConfig(
         'syntax-highlighter.engine'),
       'preserve-linebreaks' => true,
     );
   }
 
 
   /**
    * @task engine
    */
   public static function newMarkupEngine(array $options) {
     $options += self::getMarkupEngineDefaultConfiguration();
 
     $engine = new PhutilRemarkupEngine();
 
     $engine->setConfig('preserve-linebreaks', $options['preserve-linebreaks']);
     $engine->setConfig('pygments.enabled', $options['pygments']);
     $engine->setConfig(
       'uri.allowed-protocols',
       $options['uri.allowed-protocols']);
     $engine->setConfig('differential.diff', $options['differential.diff']);
     $engine->setConfig('header.generate-toc', $options['header.generate-toc']);
     $engine->setConfig(
       'syntax-highlighter.engine',
       $options['syntax-highlighter.engine']);
 
     $engine->setConfig('uri.full', $options['uri.full']);
 
     $rules = array();
     $rules[] = new PhutilRemarkupEscapeRemarkupRule();
     $rules[] = new PhutilRemarkupMonospaceRule();
 
 
     $rules[] = new PhutilRemarkupDocumentLinkRule();
     $rules[] = new PhabricatorNavigationRemarkupRule();
 
     if ($options['youtube']) {
       $rules[] = new PhabricatorYoutubeRemarkupRule();
     }
 
     $applications = PhabricatorApplication::getAllInstalledApplications();
     foreach ($applications as $application) {
       foreach ($application->getRemarkupRules() as $rule) {
         $rules[] = $rule;
       }
     }
 
     $rules[] = new PhutilRemarkupHyperlinkRule();
 
     if ($options['macros']) {
       $rules[] = new PhabricatorImageMacroRemarkupRule();
       $rules[] = new PhabricatorMemeRemarkupRule();
     }
 
     $rules[] = new PhutilRemarkupBoldRule();
     $rules[] = new PhutilRemarkupItalicRule();
     $rules[] = new PhutilRemarkupDelRule();
     $rules[] = new PhutilRemarkupUnderlineRule();
 
     foreach (self::loadCustomInlineRules() as $rule) {
       $rules[] = $rule;
     }
 
     $blocks = array();
     $blocks[] = new PhutilRemarkupQuotesBlockRule();
     $blocks[] = new PhutilRemarkupReplyBlockRule();
     $blocks[] = new PhutilRemarkupLiteralBlockRule();
     $blocks[] = new PhutilRemarkupHeaderBlockRule();
     $blocks[] = new PhutilRemarkupHorizontalRuleBlockRule();
     $blocks[] = new PhutilRemarkupListBlockRule();
     $blocks[] = new PhutilRemarkupCodeBlockRule();
     $blocks[] = new PhutilRemarkupNoteBlockRule();
     $blocks[] = new PhutilRemarkupTableBlockRule();
     $blocks[] = new PhutilRemarkupSimpleTableBlockRule();
     $blocks[] = new PhutilRemarkupInterpreterBlockRule();
     $blocks[] = new PhutilRemarkupDefaultBlockRule();
 
     foreach (self::loadCustomBlockRules() as $rule) {
       $blocks[] = $rule;
     }
 
     foreach ($blocks as $block) {
       $block->setMarkupRules($rules);
     }
 
     $engine->setBlockRules($blocks);
 
     return $engine;
   }
 
   public static function extractPHIDsFromMentions(
     PhabricatorUser $viewer,
     array $content_blocks) {
 
     $mentions = array();
 
     $engine = self::newDifferentialMarkupEngine();
     $engine->setConfig('viewer', $viewer);
 
     foreach ($content_blocks as $content_block) {
       $engine->markupText($content_block);
       $phids = $engine->getTextMetadata(
         PhabricatorMentionRemarkupRule::KEY_MENTIONED,
         array());
       $mentions += $phids;
     }
 
     return $mentions;
   }
 
   public static function extractFilePHIDsFromEmbeddedFiles(
     PhabricatorUser $viewer,
     array $content_blocks) {
     $files = array();
 
     $engine = self::newDifferentialMarkupEngine();
     $engine->setConfig('viewer', $viewer);
 
     foreach ($content_blocks as $content_block) {
       $engine->markupText($content_block);
       $phids = $engine->getTextMetadata(
         PhabricatorEmbedFileRemarkupRule::KEY_EMBED_FILE_PHIDS,
         array());
       foreach ($phids as $phid) {
         $files[$phid] = $phid;
       }
     }
 
     return array_values($files);
   }
 
   /**
    * Produce a corpus summary, in a way that shortens the underlying text
    * without truncating it somewhere awkward.
    *
    * TODO: We could do a better job of this.
    *
    * @param string  Remarkup corpus to summarize.
    * @return string Summarized corpus.
    */
   public static function summarize($corpus) {
 
     // Major goals here are:
     //  - Don't split in the middle of a character (utf-8).
     //  - Don't split in the middle of, e.g., **bold** text, since
     //    we end up with hanging '**' in the summary.
     //  - Try not to pick an image macro, header, embedded file, etc.
     //  - Hopefully don't return too much text. We don't explicitly limit
     //    this right now.
 
     $blocks = preg_split("/\n *\n\s*/", $corpus);
 
     $best = null;
     foreach ($blocks as $block) {
       // This is a test for normal spaces in the block, i.e. a heuristic to
       // distinguish standard paragraphs from things like image macros. It may
       // not work well for non-latin text. We prefer to summarize with a
       // paragraph of normal words over an image macro, if possible.
       $has_space = preg_match('/\w\s\w/', $block);
 
       // This is a test to find embedded images and headers. We prefer to
       // summarize with a normal paragraph over a header or an embedded object,
       // if possible.
       $has_embed = preg_match('/^[{=]/', $block);
 
       if ($has_space && !$has_embed) {
         // This seems like a good summary, so return it.
         return $block;
       }
 
       if (!$best) {
         // This is the first block we found; if everything is garbage just
         // use the first block.
         $best = $block;
       }
     }
 
     return $best;
   }
 
   private static function loadCustomInlineRules() {
     return id(new PhutilSymbolLoader())
       ->setAncestorClass('PhabricatorRemarkupCustomInlineRule')
       ->loadObjects();
   }
 
   private static function loadCustomBlockRules() {
     return id(new PhutilSymbolLoader())
       ->setAncestorClass('PhabricatorRemarkupCustomBlockRule')
       ->loadObjects();
   }
 
 }
diff --git a/src/infrastructure/markup/PhabricatorMarkupOneOff.php b/src/infrastructure/markup/PhabricatorMarkupOneOff.php
index 58c5d054d..7f1708745 100644
--- a/src/infrastructure/markup/PhabricatorMarkupOneOff.php
+++ b/src/infrastructure/markup/PhabricatorMarkupOneOff.php
@@ -1,94 +1,96 @@
 <?php
 
 /**
  * Concrete object for accessing the markup engine with arbitrary blobs of
  * text, like form instructions. Usage:
  *
  *   $output = PhabricatorMarkupEngine::renderOneObject(
  *     id(new PhabricatorMarkupOneOff())->setContent($some_content),
  *     'default',
  *     $viewer);
  *
  * This is less efficient than batching rendering, but appropriate for small
  * amounts of one-off text in form instructions.
  */
-final class PhabricatorMarkupOneOff implements PhabricatorMarkupInterface {
+final class PhabricatorMarkupOneOff
+  extends Phobject
+  implements PhabricatorMarkupInterface {
 
   private $content;
   private $preserveLinebreaks;
   private $engineRuleset;
   private $disableCache;
 
   public function setEngineRuleset($engine_ruleset) {
     $this->engineRuleset = $engine_ruleset;
     return $this;
   }
 
   public function getEngineRuleset() {
     return $this->engineRuleset;
   }
 
   public function setPreserveLinebreaks($preserve_linebreaks) {
     $this->preserveLinebreaks = $preserve_linebreaks;
     return $this;
   }
 
   public function setContent($content) {
     $this->content = $content;
     return $this;
   }
 
   public function getContent() {
     return $this->content;
   }
 
   public function setDisableCache($disable_cache) {
     $this->disableCache = $disable_cache;
     return $this;
   }
 
   public function getDisableCache() {
     return $this->disableCache;
   }
 
   public function getMarkupFieldKey($field) {
     return PhabricatorHash::digestForIndex($this->getContent()).':oneoff';
   }
 
   public function newMarkupEngine($field) {
     if ($this->engineRuleset) {
       return PhabricatorMarkupEngine::getEngine($this->engineRuleset);
     } else if ($this->preserveLinebreaks) {
       return PhabricatorMarkupEngine::getEngine();
     } else {
       return PhabricatorMarkupEngine::getEngine('nolinebreaks');
     }
   }
 
   public function getMarkupText($field) {
     return $this->getContent();
   }
 
   public function didMarkupText(
     $field,
     $output,
     PhutilMarkupEngine $engine) {
 
     require_celerity_resource('phabricator-remarkup-css');
     return phutil_tag(
       'div',
       array(
         'class' => 'phabricator-remarkup',
       ),
       $output);
   }
 
   public function shouldUseMarkupCache($field) {
     if ($this->getDisableCache()) {
       return false;
     }
 
     return true;
   }
 
 }
diff --git a/src/infrastructure/markup/PhabricatorSyntaxHighlighter.php b/src/infrastructure/markup/PhabricatorSyntaxHighlighter.php
index 4906f5bc3..ed3cae30a 100644
--- a/src/infrastructure/markup/PhabricatorSyntaxHighlighter.php
+++ b/src/infrastructure/markup/PhabricatorSyntaxHighlighter.php
@@ -1,31 +1,31 @@
 <?php
 
-final class PhabricatorSyntaxHighlighter {
+final class PhabricatorSyntaxHighlighter extends Phobject {
 
   public static function newEngine() {
     $engine = PhabricatorEnv::newObjectFromConfig('syntax-highlighter.engine');
 
     $config = array(
       'pygments.enabled' => PhabricatorEnv::getEnvConfig('pygments.enabled'),
       'filename.map'     => PhabricatorEnv::getEnvConfig('syntax.filemap'),
     );
 
     foreach ($config as $key => $value) {
       $engine->setConfig($key, $value);
     }
 
     return $engine;
   }
 
   public static function highlightWithFilename($filename, $source) {
     $engine = self::newEngine();
     $language = $engine->getLanguageFromFilename($filename);
     return $engine->highlightSource($language, $source);
   }
 
   public static function highlightWithLanguage($language, $source) {
     $engine = self::newEngine();
     return $engine->highlightSource($language, $source);
   }
 
 }
diff --git a/src/infrastructure/markup/interpreter/PhabricatorRemarkupFigletBlockInterpreter.php b/src/infrastructure/markup/interpreter/PhabricatorRemarkupFigletBlockInterpreter.php
index 2314b998f..c1b0d5805 100644
--- a/src/infrastructure/markup/interpreter/PhabricatorRemarkupFigletBlockInterpreter.php
+++ b/src/infrastructure/markup/interpreter/PhabricatorRemarkupFigletBlockInterpreter.php
@@ -1,45 +1,47 @@
 <?php
 
 final class PhabricatorRemarkupFigletBlockInterpreter
   extends PhutilRemarkupBlockInterpreter {
 
   public function getInterpreterName() {
     return 'figlet';
   }
 
   public function markupContent($content, array $argv) {
     if (!Filesystem::binaryExists('figlet')) {
       return $this->markupError(
-        pht('Unable to locate the `figlet` binary. Install figlet.'));
+        pht(
+          'Unable to locate the `%s` binary. Install figlet.',
+          'figlet'));
     }
 
     $font = idx($argv, 'font', 'standard');
     $safe_font = preg_replace('/[^0-9a-zA-Z-_.]/', '', $font);
     $future = id(new ExecFuture('figlet -f %s', $safe_font))
       ->setTimeout(15)
       ->write(trim($content, "\n"));
 
     list($err, $stdout, $stderr) = $future->resolve();
 
     if ($err) {
       return $this->markupError(
         pht(
           'Execution of `%s` failed: %s',
           'figlet',
           $stderr));
     }
 
 
     if ($this->getEngine()->isTextMode()) {
       return $stdout;
     }
 
     return phutil_tag(
       'div',
       array(
         'class' => 'PhabricatorMonospaced remarkup-figlet',
       ),
       $stdout);
   }
 
 }
diff --git a/src/infrastructure/markup/interpreter/PhabricatorRemarkupGraphvizBlockInterpreter.php b/src/infrastructure/markup/interpreter/PhabricatorRemarkupGraphvizBlockInterpreter.php
index d18ebbad1..e06432be6 100644
--- a/src/infrastructure/markup/interpreter/PhabricatorRemarkupGraphvizBlockInterpreter.php
+++ b/src/infrastructure/markup/interpreter/PhabricatorRemarkupGraphvizBlockInterpreter.php
@@ -1,61 +1,64 @@
 <?php
 
 final class PhabricatorRemarkupGraphvizBlockInterpreter
   extends PhutilRemarkupBlockInterpreter {
 
   public function getInterpreterName() {
     return 'dot';
   }
 
   public function markupContent($content, array $argv) {
     if (!Filesystem::binaryExists('dot')) {
       return $this->markupError(
-        pht('Unable to locate the `dot` binary. Install Graphviz.'));
+        pht(
+          'Unable to locate the `%s` binary. Install Graphviz.',
+          'dot'));
     }
 
     $width = $this->parseDimension(idx($argv, 'width'));
 
     $future = id(new ExecFuture('dot -T%s', 'png'))
       ->setTimeout(15)
       ->write(trim($content));
 
     list($err, $stdout, $stderr) = $future->resolve();
 
     if ($err) {
       return $this->markupError(
         pht(
-          'Execution of `dot` failed (#%d), check your syntax: %s',
+          'Execution of `%s` failed (#%d), check your syntax: %s',
+          'dot',
           $err,
           $stderr));
     }
 
     $file = PhabricatorFile::buildFromFileDataOrHash(
       $stdout,
       array(
         'name' => 'graphviz.png',
       ));
 
     if ($this->getEngine()->isTextMode()) {
       return '<'.$file->getBestURI().'>';
     }
 
     return phutil_tag(
       'img',
       array(
         'src' => $file->getBestURI(),
         'width' => nonempty($width, null),
       ));
   }
 
   // TODO: This is duplicated from PhabricatorEmbedFileRemarkupRule since they
   // do not share a base class.
   private function parseDimension($string) {
     $string = trim($string);
 
     if (preg_match('/^(?:\d*\\.)?\d+%?$/', $string)) {
       return $string;
     }
 
     return null;
   }
 }
diff --git a/src/infrastructure/markup/rule/PhabricatorYoutubeRemarkupRule.php b/src/infrastructure/markup/rule/PhabricatorYoutubeRemarkupRule.php
index 43af8d3f0..bad8e0eac 100644
--- a/src/infrastructure/markup/rule/PhabricatorYoutubeRemarkupRule.php
+++ b/src/infrastructure/markup/rule/PhabricatorYoutubeRemarkupRule.php
@@ -1,49 +1,51 @@
 <?php
 
 final class PhabricatorYoutubeRemarkupRule extends PhutilRemarkupRule {
 
+  private $uri;
+
   public function getPriority() {
     return 350.0;
   }
 
   public function apply($text) {
     $this->uri = new PhutilURI($text);
 
     if ($this->uri->getDomain() &&
         preg_match('/(^|\.)youtube\.com$/', $this->uri->getDomain()) &&
         idx($this->uri->getQueryParams(), 'v')) {
       return $this->markupYoutubeLink();
     }
 
     return $text;
   }
 
   public function markupYoutubeLink() {
     $v = idx($this->uri->getQueryParams(), 'v');
     $text_mode = $this->getEngine()->isTextMode();
     $mail_mode = $this->getEngine()->isHTMLMailMode();
 
     if ($text_mode || $mail_mode) {
       return $this->getEngine()->storeText('http://youtu.be/'.$v);
     }
 
     $youtube_src = 'https://www.youtube.com/embed/'.$v;
     $iframe = $this->newTag(
       'div',
       array(
         'class' => 'embedded-youtube-video',
       ),
       $this->newTag(
         'iframe',
         array(
           'width'       => '650',
           'height'      => '400',
           'style'       => 'margin: 1em auto; border: 0px;',
           'src'         => $youtube_src,
           'frameborder' => 0,
         ),
         ''));
     return $this->getEngine()->storeText($iframe);
   }
 
 }
diff --git a/src/infrastructure/query/PhabricatorQuery.php b/src/infrastructure/query/PhabricatorQuery.php
index f57c43c0e..5893297db 100644
--- a/src/infrastructure/query/PhabricatorQuery.php
+++ b/src/infrastructure/query/PhabricatorQuery.php
@@ -1,122 +1,122 @@
 <?php
 
 /**
  * @task format Formatting Query Clauses
  */
-abstract class PhabricatorQuery {
+abstract class PhabricatorQuery extends Phobject {
 
 
   abstract public function execute();
 
 
 /* -(  Formatting Query Clauses  )------------------------------------------- */
 
 
   /**
    * @task format
    */
   protected function formatWhereClause(array $parts) {
     $parts = $this->flattenSubclause($parts);
     if (!$parts) {
       return '';
     }
 
     return 'WHERE '.$this->formatWhereSubclause($parts);
   }
 
 
   /**
    * @task format
    */
   protected function formatWhereSubclause(array $parts) {
     $parts = $this->flattenSubclause($parts);
     if (!$parts) {
       return null;
     }
 
     return '('.implode(') AND (', $parts).')';
   }
 
 
   /**
    * @task format
    */
   protected function formatSelectClause(array $parts) {
     $parts = $this->flattenSubclause($parts);
     if (!$parts) {
       throw new Exception(pht('Can not build empty select clause!'));
     }
 
     return 'SELECT '.$this->formatSelectSubclause($parts);
   }
 
 
   /**
    * @task format
    */
   protected function formatSelectSubclause(array $parts) {
     $parts = $this->flattenSubclause($parts);
     if (!$parts) {
       return null;
     }
     return implode(', ', $parts);
   }
 
 
   /**
    * @task format
    */
   protected function formatJoinClause(array $parts) {
     $parts = $this->flattenSubclause($parts);
     if (!$parts) {
       return '';
     }
 
     return implode(' ', $parts);
   }
 
 
   /**
    * @task format
    */
   protected function formatHavingClause(array $parts) {
     $parts = $this->flattenSubclause($parts);
     if (!$parts) {
       return '';
     }
 
     return 'HAVING '.$this->formatHavingSubclause($parts);
   }
 
 
   /**
    * @task format
    */
   protected function formatHavingSubclause(array $parts) {
     $parts = $this->flattenSubclause($parts);
     if (!$parts) {
       return null;
     }
 
     return '('.implode(') AND (', $parts).')';
   }
 
 
   /**
    * @task format
    */
   private function flattenSubclause(array $parts) {
     $result = array();
     foreach ($parts as $part) {
       if (is_array($part)) {
         foreach ($this->flattenSubclause($part) as $subpart) {
           $result[] = $subpart;
         }
       } else if (strlen($part)) {
         $result[] = $part;
       }
     }
     return $result;
   }
 
 }
diff --git a/src/infrastructure/query/policy/PhabricatorCursorPagedPolicyAwareQuery.php b/src/infrastructure/query/policy/PhabricatorCursorPagedPolicyAwareQuery.php
index b48e1e5c4..4d0f96bde 100644
--- a/src/infrastructure/query/policy/PhabricatorCursorPagedPolicyAwareQuery.php
+++ b/src/infrastructure/query/policy/PhabricatorCursorPagedPolicyAwareQuery.php
@@ -1,1841 +1,1852 @@
 <?php
 
 /**
  * A query class which uses cursor-based paging. This paging is much more
  * performant than offset-based paging in the presence of policy filtering.
  *
  * @task clauses Building Query Clauses
  * @task appsearch Integration with ApplicationSearch
  * @task customfield Integration with CustomField
  * @task paging Paging
  * @task order Result Ordering
  * @task edgelogic Working with Edge Logic
  * @task spaces Working with Spaces
  */
 abstract class PhabricatorCursorPagedPolicyAwareQuery
   extends PhabricatorPolicyAwareQuery {
 
   private $afterID;
   private $beforeID;
   private $applicationSearchConstraints = array();
   private $internalPaging;
   private $orderVector;
   private $groupVector;
   private $builtinOrder;
   private $edgeLogicConstraints = array();
   private $edgeLogicConstraintsAreValid = false;
   private $spacePHIDs;
+  private $spaceIsArchived;
 
   protected function getPageCursors(array $page) {
     return array(
       $this->getResultCursor(head($page)),
       $this->getResultCursor(last($page)),
     );
   }
 
   protected function getResultCursor($object) {
     if (!is_object($object)) {
       throw new Exception(
         pht(
           'Expected object, got "%s".',
           gettype($object)));
     }
 
     return $object->getID();
   }
 
   protected function nextPage(array $page) {
     // See getPagingViewer() for a description of this flag.
     $this->internalPaging = true;
 
     if ($this->beforeID !== null) {
       $page = array_reverse($page, $preserve_keys = true);
       list($before, $after) = $this->getPageCursors($page);
       $this->beforeID = $before;
     } else {
       list($before, $after) = $this->getPageCursors($page);
       $this->afterID = $after;
     }
   }
 
   final public function setAfterID($object_id) {
     $this->afterID = $object_id;
     return $this;
   }
 
   final protected function getAfterID() {
     return $this->afterID;
   }
 
   final public function setBeforeID($object_id) {
     $this->beforeID = $object_id;
     return $this;
   }
 
   final protected function getBeforeID() {
     return $this->beforeID;
   }
 
   protected function loadStandardPage(PhabricatorLiskDAO $table) {
     $rows = $this->loadStandardPageRows($table);
     return $table->loadAllFromArray($rows);
   }
 
   protected function loadStandardPageRows(PhabricatorLiskDAO $table) {
     $conn = $table->establishConnection('r');
 
     $rows = queryfx_all(
       $conn,
       '%Q FROM %T %Q %Q %Q %Q %Q %Q %Q',
       $this->buildSelectClause($conn),
       $table->getTableName(),
       (string)$this->getPrimaryTableAlias(),
       $this->buildJoinClause($conn),
       $this->buildWhereClause($conn),
       $this->buildGroupClause($conn),
       $this->buildHavingClause($conn),
       $this->buildOrderClause($conn),
       $this->buildLimitClause($conn));
 
     return $rows;
   }
 
   /**
    * Get the viewer for making cursor paging queries.
    *
    * NOTE: You should ONLY use this viewer to load cursor objects while
    * building paging queries.
    *
    * Cursor paging can happen in two ways. First, the user can request a page
    * like `/stuff/?after=33`, which explicitly causes paging. Otherwise, we
    * can fall back to implicit paging if we filter some results out of a
    * result list because the user can't see them and need to go fetch some more
    * results to generate a large enough result list.
    *
    * In the first case, want to use the viewer's policies to load the object.
    * This prevents an attacker from figuring out information about an object
    * they can't see by executing queries like `/stuff/?after=33&order=name`,
    * which would otherwise give them a hint about the name of the object.
    * Generally, if a user can't see an object, they can't use it to page.
    *
    * In the second case, we need to load the object whether the user can see
    * it or not, because we need to examine new results. For example, if a user
    * loads `/stuff/` and we run a query for the first 100 items that they can
    * see, but the first 100 rows in the database aren't visible, we need to
    * be able to issue a query for the next 100 results. If we can't load the
    * cursor object, we'll fail or issue the same query over and over again.
    * So, generally, internal paging must bypass policy controls.
    *
    * This method returns the appropriate viewer, based on the context in which
    * the paging is occuring.
    *
    * @return PhabricatorUser Viewer for executing paging queries.
    */
   final protected function getPagingViewer() {
     if ($this->internalPaging) {
       return PhabricatorUser::getOmnipotentUser();
     } else {
       return $this->getViewer();
     }
   }
 
   final protected function buildLimitClause(AphrontDatabaseConnection $conn_r) {
     if ($this->getRawResultLimit()) {
       return qsprintf($conn_r, 'LIMIT %d', $this->getRawResultLimit());
     } else {
       return '';
     }
   }
 
   final protected function didLoadResults(array $results) {
     if ($this->beforeID) {
       $results = array_reverse($results, $preserve_keys = true);
     }
     return $results;
   }
 
   final public function executeWithCursorPager(AphrontCursorPagerView $pager) {
     $limit = $pager->getPageSize();
 
     $this->setLimit($limit + 1);
 
     if ($pager->getAfterID()) {
       $this->setAfterID($pager->getAfterID());
     } else if ($pager->getBeforeID()) {
       $this->setBeforeID($pager->getBeforeID());
     }
 
     $results = $this->execute();
     $count = count($results);
 
     $sliced_results = $pager->sliceResults($results);
     if ($sliced_results) {
       list($before, $after) = $this->getPageCursors($sliced_results);
 
       if ($pager->getBeforeID() || ($count > $limit)) {
         $pager->setNextPageID($after);
       }
 
       if ($pager->getAfterID() ||
          ($pager->getBeforeID() && ($count > $limit))) {
         $pager->setPrevPageID($before);
       }
     }
 
     return $sliced_results;
   }
 
 
   /**
    * Return the alias this query uses to identify the primary table.
    *
    * Some automatic query constructions may need to be qualified with a table
    * alias if the query performs joins which make column names ambiguous. If
    * this is the case, return the alias for the primary table the query
    * uses; generally the object table which has `id` and `phid` columns.
    *
    * @return string Alias for the primary table.
    */
   protected function getPrimaryTableAlias() {
     return null;
   }
 
   public function newResultObject() {
     return null;
   }
 
 
 /* -(  Building Query Clauses  )--------------------------------------------- */
 
 
   /**
    * @task clauses
    */
   protected function buildSelectClause(AphrontDatabaseConnection $conn) {
     $parts = $this->buildSelectClauseParts($conn);
     return $this->formatSelectClause($parts);
   }
 
 
   /**
    * @task clauses
    */
   protected function buildSelectClauseParts(AphrontDatabaseConnection $conn) {
     $select = array();
 
     $alias = $this->getPrimaryTableAlias();
     if ($alias) {
       $select[] = qsprintf($conn, '%T.*', $alias);
     } else {
       $select[] = '*';
     }
 
     $select[] = $this->buildEdgeLogicSelectClause($conn);
 
     return $select;
   }
 
 
   /**
    * @task clauses
    */
   protected function buildJoinClause(AphrontDatabaseConnection $conn) {
     $joins = $this->buildJoinClauseParts($conn);
     return $this->formatJoinClause($joins);
   }
 
 
   /**
    * @task clauses
    */
   protected function buildJoinClauseParts(AphrontDatabaseConnection $conn) {
     $joins = array();
     $joins[] = $this->buildEdgeLogicJoinClause($conn);
     $joins[] = $this->buildApplicationSearchJoinClause($conn);
     return $joins;
   }
 
 
   /**
    * @task clauses
    */
   protected function buildWhereClause(AphrontDatabaseConnection $conn) {
     $where = $this->buildWhereClauseParts($conn);
     return $this->formatWhereClause($where);
   }
 
 
   /**
    * @task clauses
    */
   protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) {
     $where = array();
     $where[] = $this->buildPagingClause($conn);
     $where[] = $this->buildEdgeLogicWhereClause($conn);
     $where[] = $this->buildSpacesWhereClause($conn);
     return $where;
   }
 
 
   /**
    * @task clauses
    */
   protected function buildHavingClause(AphrontDatabaseConnection $conn) {
     $having = $this->buildHavingClauseParts($conn);
     return $this->formatHavingClause($having);
   }
 
 
   /**
    * @task clauses
    */
   protected function buildHavingClauseParts(AphrontDatabaseConnection $conn) {
     $having = array();
     $having[] = $this->buildEdgeLogicHavingClause($conn);
     return $having;
   }
 
 
   /**
    * @task clauses
    */
   protected function buildGroupClause(AphrontDatabaseConnection $conn) {
     if (!$this->shouldGroupQueryResultRows()) {
       return '';
     }
 
     return qsprintf(
       $conn,
       'GROUP BY %Q',
       $this->getApplicationSearchObjectPHIDColumn());
   }
 
 
   /**
    * @task clauses
    */
   protected function shouldGroupQueryResultRows() {
     if ($this->shouldGroupEdgeLogicResultRows()) {
       return true;
     }
 
     if ($this->getApplicationSearchMayJoinMultipleRows()) {
       return true;
     }
 
     return false;
   }
 
 
 
 /* -(  Paging  )------------------------------------------------------------- */
 
 
   /**
    * @task paging
    */
   protected function buildPagingClause(AphrontDatabaseConnection $conn) {
     $orderable = $this->getOrderableColumns();
     $vector = $this->getOrderVector();
 
     if ($this->beforeID !== null) {
       $cursor = $this->beforeID;
       $reversed = true;
     } else if ($this->afterID !== null) {
       $cursor = $this->afterID;
       $reversed = false;
     } else {
       // No paging is being applied to this query so we do not need to
       // construct a paging clause.
       return '';
     }
 
     $keys = array();
     foreach ($vector as $order) {
       $keys[] = $order->getOrderKey();
     }
 
     $value_map = $this->getPagingValueMap($cursor, $keys);
 
     $columns = array();
     foreach ($vector as $order) {
       $key = $order->getOrderKey();
 
       if (!array_key_exists($key, $value_map)) {
         throw new Exception(
           pht(
             'Query "%s" failed to return a value from getPagingValueMap() '.
             'for column "%s".',
             get_class($this),
             $key));
       }
 
       $column = $orderable[$key];
       $column['value'] = $value_map[$key];
 
       $columns[] = $column;
     }
 
     return $this->buildPagingClauseFromMultipleColumns(
       $conn,
       $columns,
       array(
         'reversed' => $reversed,
       ));
   }
 
 
   /**
    * @task paging
    */
   protected function getPagingValueMap($cursor, array $keys) {
     return array(
       'id' => $cursor,
     );
   }
 
 
   /**
    * @task paging
    */
   protected function loadCursorObject($cursor) {
     $query = newv(get_class($this), array())
       ->setViewer($this->getPagingViewer())
       ->withIDs(array((int)$cursor));
 
     $this->willExecuteCursorQuery($query);
 
     $object = $query->executeOne();
     if (!$object) {
       throw new Exception(
         pht(
           'Cursor "%s" does not identify a valid object.',
           $cursor));
     }
 
     return $object;
   }
 
 
   /**
    * @task paging
    */
   protected function willExecuteCursorQuery(
     PhabricatorCursorPagedPolicyAwareQuery $query) {
     return;
   }
 
 
   /**
    * Simplifies the task of constructing a paging clause across multiple
    * columns. In the general case, this looks like:
    *
    *   A > a OR (A = a AND B > b) OR (A = a AND B = b AND C > c)
    *
    * To build a clause, specify the name, type, and value of each column
    * to include:
    *
    *   $this->buildPagingClauseFromMultipleColumns(
    *     $conn_r,
    *     array(
    *       array(
    *         'table' => 't',
    *         'column' => 'title',
    *         'type' => 'string',
    *         'value' => $cursor->getTitle(),
    *         'reverse' => true,
    *       ),
    *       array(
    *         'table' => 't',
    *         'column' => 'id',
    *         'type' => 'int',
    *         'value' => $cursor->getID(),
    *       ),
    *     ),
    *     array(
    *       'reversed' => $is_reversed,
    *     ));
    *
    * This method will then return a composable clause for inclusion in WHERE.
    *
    * @param AphrontDatabaseConnection Connection query will execute on.
    * @param list<map> Column description dictionaries.
    * @param map Additional constuction options.
    * @return string Query clause.
    * @task paging
    */
   final protected function buildPagingClauseFromMultipleColumns(
     AphrontDatabaseConnection $conn,
     array $columns,
     array $options) {
 
     foreach ($columns as $column) {
       PhutilTypeSpec::checkMap(
         $column,
         array(
           'table' => 'optional string|null',
           'column' => 'string',
           'value' => 'wild',
           'type' => 'string',
           'reverse' => 'optional bool',
           'unique' => 'optional bool',
           'null' => 'optional string|null',
         ));
     }
 
     PhutilTypeSpec::checkMap(
       $options,
       array(
         'reversed' => 'optional bool',
       ));
 
     $is_query_reversed = idx($options, 'reversed', false);
 
     $clauses = array();
     $accumulated = array();
     $last_key = last_key($columns);
     foreach ($columns as $key => $column) {
       $type = $column['type'];
 
       $null = idx($column, 'null');
       if ($column['value'] === null) {
         if ($null) {
           $value = null;
         } else {
           throw new Exception(
             pht(
               'Column "%s" has null value, but does not specify a null '.
               'behavior.',
               $key));
         }
       } else {
         switch ($type) {
           case 'int':
             $value = qsprintf($conn, '%d', $column['value']);
             break;
           case 'float':
             $value = qsprintf($conn, '%f', $column['value']);
             break;
           case 'string':
             $value = qsprintf($conn, '%s', $column['value']);
             break;
           default:
             throw new Exception(
               pht(
                 'Column "%s" has unknown column type "%s".',
                 $column['column'],
                 $type));
         }
       }
 
       $is_column_reversed = idx($column, 'reverse', false);
       $reverse = ($is_query_reversed xor $is_column_reversed);
 
       $clause = $accumulated;
 
       $table_name = idx($column, 'table');
       $column_name = $column['column'];
       if ($table_name !== null) {
         $field = qsprintf($conn, '%T.%T', $table_name, $column_name);
       } else {
         $field = qsprintf($conn, '%T', $column_name);
       }
 
       $parts = array();
       if ($null) {
         $can_page_if_null = ($null === 'head');
         $can_page_if_nonnull = ($null === 'tail');
 
         if ($reverse) {
           $can_page_if_null = !$can_page_if_null;
           $can_page_if_nonnull = !$can_page_if_nonnull;
         }
 
         $subclause = null;
         if ($can_page_if_null && $value === null) {
           $parts[] = qsprintf(
             $conn,
             '(%Q IS NOT NULL)',
             $field);
         } else if ($can_page_if_nonnull && $value !== null) {
           $parts[] = qsprintf(
             $conn,
             '(%Q IS NULL)',
             $field);
         }
       }
 
       if ($value !== null) {
         $parts[] = qsprintf(
           $conn,
           '%Q %Q %Q',
           $field,
           $reverse ? '>' : '<',
           $value);
       }
 
       if ($parts) {
         if (count($parts) > 1) {
           $clause[] = '('.implode(') OR (', $parts).')';
         } else {
           $clause[] = head($parts);
         }
       }
 
       if ($clause) {
         if (count($clause) > 1) {
           $clauses[] = '('.implode(') AND (', $clause).')';
         } else {
           $clauses[] = head($clause);
         }
       }
 
       if ($value === null) {
         $accumulated[] = qsprintf(
           $conn,
           '%Q IS NULL',
           $field);
       } else {
         $accumulated[] = qsprintf(
           $conn,
           '%Q = %Q',
           $field,
           $value);
       }
     }
 
     return '('.implode(') OR (', $clauses).')';
   }
 
 
 /* -(  Result Ordering  )---------------------------------------------------- */
 
 
   /**
    * Select a result ordering.
    *
    * This is a high-level method which selects an ordering from a predefined
    * list of builtin orders, as provided by @{method:getBuiltinOrders}. These
    * options are user-facing and not exhaustive, but are generally convenient
    * and meaningful.
    *
    * You can also use @{method:setOrderVector} to specify a low-level ordering
    * across individual orderable columns. This offers greater control but is
    * also more involved.
    *
    * @param string Key of a builtin order supported by this query.
    * @return this
    * @task order
    */
   public function setOrder($order) {
     $aliases = $this->getBuiltinOrderAliasMap();
 
     if (empty($aliases[$order])) {
       throw new Exception(
         pht(
           'Query "%s" does not support a builtin order "%s". Supported orders '.
           'are: %s.',
           get_class($this),
           $order,
           implode(', ', array_keys($aliases))));
     }
 
     $this->builtinOrder = $aliases[$order];
     $this->orderVector = null;
 
     return $this;
   }
 
 
   /**
    * Set a grouping order to apply before primary result ordering.
    *
    * This allows you to preface the query order vector with additional orders,
    * so you can effect "group by" queries while still respecting "order by".
    *
    * This is a high-level method which works alongside @{method:setOrder}. For
    * lower-level control over order vectors, use @{method:setOrderVector}.
    *
    * @param PhabricatorQueryOrderVector|list<string> List of order keys.
    * @return this
    * @task order
    */
   public function setGroupVector($vector) {
     $this->groupVector = $vector;
     $this->orderVector = null;
 
     return $this;
   }
 
 
   /**
    * Get builtin orders for this class.
    *
    * In application UIs, we want to be able to present users with a small
    * selection of meaningful order options (like "Order by Title") rather than
    * an exhaustive set of column ordering options.
    *
    * Meaningful user-facing orders are often really orders across multiple
    * columns: for example, a "title" ordering is usually implemented as a
    * "title, id" ordering under the hood.
    *
    * Builtin orders provide a mapping from convenient, understandable
    * user-facing orders to implementations.
    *
    * A builtin order should provide these keys:
    *
    *   - `vector` (`list<string>`): The actual order vector to use.
    *   - `name` (`string`): Human-readable order name.
    *
    * @return map<string, wild> Map from builtin order keys to specification.
    * @task order
    */
   public function getBuiltinOrders() {
     $orders = array(
       'newest' => array(
         'vector' => array('id'),
         'name' => pht('Creation (Newest First)'),
         'aliases' => array('created'),
       ),
       'oldest' => array(
         'vector' => array('-id'),
         'name' => pht('Creation (Oldest First)'),
       ),
     );
 
     $object = $this->newResultObject();
     if ($object instanceof PhabricatorCustomFieldInterface) {
       $list = PhabricatorCustomField::getObjectFields(
         $object,
         PhabricatorCustomField::ROLE_APPLICATIONSEARCH);
       foreach ($list->getFields() as $field) {
         $index = $field->buildOrderIndex();
         if (!$index) {
           continue;
         }
 
         $key = $field->getFieldKey();
         $digest = $field->getFieldIndex();
 
         $full_key = 'custom:'.$key;
         $orders[$full_key] = array(
           'vector' => array($full_key, 'id'),
           'name' => $field->getFieldName(),
         );
       }
     }
 
     return $orders;
   }
 
   public function getBuiltinOrderAliasMap() {
     $orders = $this->getBuiltinOrders();
 
     $map = array();
     foreach ($orders as $key => $order) {
       $keys = array();
       $keys[] = $key;
       foreach (idx($order, 'aliases', array()) as $alias) {
         $keys[] = $alias;
       }
 
       foreach ($keys as $alias) {
         if (isset($map[$alias])) {
           throw new Exception(
             pht(
               'Two builtin orders ("%s" and "%s") define the same key or '.
               'alias ("%s"). Each order alias and key must be unique and '.
               'identify a single order.',
               $key,
               $map[$alias],
               $alias));
         }
         $map[$alias] = $key;
       }
     }
 
     return $map;
   }
 
 
   /**
    * Set a low-level column ordering.
    *
    * This is a low-level method which offers granular control over column
    * ordering. In most cases, applications can more easily use
    * @{method:setOrder} to choose a high-level builtin order.
    *
    * To set an order vector, specify a list of order keys as provided by
    * @{method:getOrderableColumns}.
    *
    * @param PhabricatorQueryOrderVector|list<string> List of order keys.
    * @return this
    * @task order
    */
   public function setOrderVector($vector) {
     $vector = PhabricatorQueryOrderVector::newFromVector($vector);
 
     $orderable = $this->getOrderableColumns();
 
     // Make sure that all the components identify valid columns.
     $unique = array();
     foreach ($vector as $order) {
       $key = $order->getOrderKey();
       if (empty($orderable[$key])) {
         $valid = implode(', ', array_keys($orderable));
         throw new Exception(
           pht(
             'This query ("%s") does not support sorting by order key "%s". '.
             'Supported orders are: %s.',
             get_class($this),
             $key,
             $valid));
       }
 
       $unique[$key] = idx($orderable[$key], 'unique', false);
     }
 
     // Make sure that the last column is unique so that this is a strong
     // ordering which can be used for paging.
     $last = last($unique);
     if ($last !== true) {
       throw new Exception(
         pht(
           'Order vector "%s" is invalid: the last column in an order must '.
           'be a column with unique values, but "%s" is not unique.',
           $vector->getAsString(),
           last_key($unique)));
     }
 
     // Make sure that other columns are not unique; an ordering like "id, name"
     // does not make sense because only "id" can ever have an effect.
     array_pop($unique);
     foreach ($unique as $key => $is_unique) {
       if ($is_unique) {
         throw new Exception(
           pht(
             'Order vector "%s" is invalid: only the last column in an order '.
             'may be unique, but "%s" is a unique column and not the last '.
             'column in the order.',
             $vector->getAsString(),
             $key));
       }
     }
 
     $this->orderVector = $vector;
     return $this;
   }
 
 
   /**
    * Get the effective order vector.
    *
    * @return PhabricatorQueryOrderVector Effective vector.
    * @task order
    */
   protected function getOrderVector() {
     if (!$this->orderVector) {
       if ($this->builtinOrder !== null) {
         $builtin_order = idx($this->getBuiltinOrders(), $this->builtinOrder);
         $vector = $builtin_order['vector'];
       } else {
         $vector = $this->getDefaultOrderVector();
       }
 
       if ($this->groupVector) {
         $group = PhabricatorQueryOrderVector::newFromVector($this->groupVector);
         $group->appendVector($vector);
         $vector = $group;
       }
 
       $vector = PhabricatorQueryOrderVector::newFromVector($vector);
 
       // We call setOrderVector() here to apply checks to the default vector.
       // This catches any errors in the implementation.
       $this->setOrderVector($vector);
     }
 
     return $this->orderVector;
   }
 
 
   /**
    * @task order
    */
   protected function getDefaultOrderVector() {
     return array('id');
   }
 
 
   /**
    * @task order
    */
   public function getOrderableColumns() {
     $columns = array(
       'id' => array(
         'table' => $this->getPrimaryTableAlias(),
         'column' => 'id',
         'reverse' => false,
         'type' => 'int',
         'unique' => true,
       ),
     );
 
     $object = $this->newResultObject();
     if ($object instanceof PhabricatorCustomFieldInterface) {
       $list = PhabricatorCustomField::getObjectFields(
         $object,
         PhabricatorCustomField::ROLE_APPLICATIONSEARCH);
       foreach ($list->getFields() as $field) {
         $index = $field->buildOrderIndex();
         if (!$index) {
           continue;
         }
 
         $key = $field->getFieldKey();
         $digest = $field->getFieldIndex();
 
         $full_key = 'custom:'.$key;
         $columns[$full_key] = array(
           'table' => 'appsearch_order_'.$digest,
           'column' => 'indexValue',
           'type' => $index->getIndexValueType(),
           'null' => 'tail',
           'customfield' => true,
           'customfield.index.table' => $index->getTableName(),
           'customfield.index.key' => $digest,
         );
       }
     }
 
     return $columns;
   }
 
 
   /**
    * @task order
    */
   final protected function buildOrderClause(AphrontDatabaseConnection $conn) {
     $orderable = $this->getOrderableColumns();
     $vector = $this->getOrderVector();
 
     $parts = array();
     foreach ($vector as $order) {
       $part = $orderable[$order->getOrderKey()];
       if ($order->getIsReversed()) {
         $part['reverse'] = !idx($part, 'reverse', false);
       }
       $parts[] = $part;
     }
 
     return $this->formatOrderClause($conn, $parts);
   }
 
 
   /**
    * @task order
    */
   protected function formatOrderClause(
     AphrontDatabaseConnection $conn,
     array $parts) {
 
     $is_query_reversed = false;
     if ($this->getBeforeID()) {
       $is_query_reversed = !$is_query_reversed;
     }
 
     $sql = array();
     foreach ($parts as $key => $part) {
       $is_column_reversed = !empty($part['reverse']);
 
       $descending = true;
       if ($is_query_reversed) {
         $descending = !$descending;
       }
 
       if ($is_column_reversed) {
         $descending = !$descending;
       }
 
       $table = idx($part, 'table');
       $column = $part['column'];
 
       if ($table !== null) {
         $field = qsprintf($conn, '%T.%T', $table, $column);
       } else {
         $field = qsprintf($conn, '%T', $column);
       }
 
       $null = idx($part, 'null');
       if ($null) {
         switch ($null) {
           case 'head':
             $null_field = qsprintf($conn, '(%Q IS NULL)', $field);
             break;
           case 'tail':
             $null_field = qsprintf($conn, '(%Q IS NOT NULL)', $field);
             break;
           default:
             throw new Exception(
               pht(
                 'NULL value "%s" is invalid. Valid values are "head" and '.
                 '"tail".',
                 $null));
         }
 
         if ($descending) {
           $sql[] = qsprintf($conn, '%Q DESC', $null_field);
         } else {
           $sql[] = qsprintf($conn, '%Q ASC', $null_field);
         }
       }
 
       if ($descending) {
         $sql[] = qsprintf($conn, '%Q DESC', $field);
       } else {
         $sql[] = qsprintf($conn, '%Q ASC', $field);
       }
     }
 
     return qsprintf($conn, 'ORDER BY %Q', implode(', ', $sql));
   }
 
 
 /* -(  Application Search  )------------------------------------------------- */
 
 
   /**
    * Constrain the query with an ApplicationSearch index, requiring field values
    * contain at least one of the values in a set.
    *
    * This constraint can build the most common types of queries, like:
    *
    *   - Find users with shirt sizes "X" or "XL".
    *   - Find shoes with size "13".
    *
    * @param PhabricatorCustomFieldIndexStorage Table where the index is stored.
    * @param string|list<string> One or more values to filter by.
    * @return this
    * @task appsearch
    */
   public function withApplicationSearchContainsConstraint(
     PhabricatorCustomFieldIndexStorage $index,
     $value) {
 
     $this->applicationSearchConstraints[] = array(
       'type'  => $index->getIndexValueType(),
       'cond'  => '=',
       'table' => $index->getTableName(),
       'index' => $index->getIndexKey(),
       'value' => $value,
     );
 
     return $this;
   }
 
 
   /**
    * Constrain the query with an ApplicationSearch index, requiring values
    * exist in a given range.
    *
    * This constraint is useful for expressing date ranges:
    *
    *   - Find events between July 1st and July 7th.
    *
    * The ends of the range are inclusive, so a `$min` of `3` and a `$max` of
    * `5` will match fields with values `3`, `4`, or `5`. Providing `null` for
    * either end of the range will leave that end of the constraint open.
    *
    * @param PhabricatorCustomFieldIndexStorage Table where the index is stored.
    * @param int|null Minimum permissible value, inclusive.
    * @param int|null Maximum permissible value, inclusive.
    * @return this
    * @task appsearch
    */
   public function withApplicationSearchRangeConstraint(
     PhabricatorCustomFieldIndexStorage $index,
     $min,
     $max) {
 
     $index_type = $index->getIndexValueType();
     if ($index_type != 'int') {
       throw new Exception(
         pht(
           'Attempting to apply a range constraint to a field with index type '.
           '"%s", expected type "%s".',
           $index_type,
           'int'));
     }
 
     $this->applicationSearchConstraints[] = array(
       'type' => $index->getIndexValueType(),
       'cond' => 'range',
       'table' => $index->getTableName(),
       'index' => $index->getIndexKey(),
       'value' => array($min, $max),
     );
 
     return $this;
   }
 
 
   /**
    * Get the name of the query's primary object PHID column, for constructing
    * JOIN clauses. Normally (and by default) this is just `"phid"`, but it may
    * be something more exotic.
    *
    * See @{method:getPrimaryTableAlias} if the column needs to be qualified with
    * a table alias.
    *
    * @return string Column name.
    * @task appsearch
    */
   protected function getApplicationSearchObjectPHIDColumn() {
     if ($this->getPrimaryTableAlias()) {
       $prefix = $this->getPrimaryTableAlias().'.';
     } else {
       $prefix = '';
     }
 
     return $prefix.'phid';
   }
 
 
   /**
    * Determine if the JOINs built by ApplicationSearch might cause each primary
    * object to return multiple result rows. Generally, this means the query
    * needs an extra GROUP BY clause.
    *
    * @return bool True if the query may return multiple rows for each object.
    * @task appsearch
    */
   protected function getApplicationSearchMayJoinMultipleRows() {
     foreach ($this->applicationSearchConstraints as $constraint) {
       $type = $constraint['type'];
       $value = $constraint['value'];
       $cond = $constraint['cond'];
 
       switch ($cond) {
         case '=':
           switch ($type) {
             case 'string':
             case 'int':
               if (count((array)$value) > 1) {
                 return true;
               }
               break;
             default:
               throw new Exception(pht('Unknown index type "%s"!', $type));
           }
           break;
         case 'range':
           // NOTE: It's possible to write a custom field where multiple rows
           // match a range constraint, but we don't currently ship any in the
           // upstream and I can't immediately come up with cases where this
           // would make sense.
           break;
         default:
           throw new Exception(pht('Unknown constraint condition "%s"!', $cond));
       }
     }
 
     return false;
   }
 
 
   /**
    * Construct a GROUP BY clause appropriate for ApplicationSearch constraints.
    *
    * @param AphrontDatabaseConnection Connection executing the query.
    * @return string Group clause.
    * @task appsearch
    */
   protected function buildApplicationSearchGroupClause(
     AphrontDatabaseConnection $conn_r) {
 
     if ($this->getApplicationSearchMayJoinMultipleRows()) {
       return qsprintf(
         $conn_r,
         'GROUP BY %Q',
         $this->getApplicationSearchObjectPHIDColumn());
     } else {
       return '';
     }
   }
 
 
   /**
    * Construct a JOIN clause appropriate for applying ApplicationSearch
    * constraints.
    *
    * @param AphrontDatabaseConnection Connection executing the query.
    * @return string Join clause.
    * @task appsearch
    */
   protected function buildApplicationSearchJoinClause(
     AphrontDatabaseConnection $conn_r) {
 
     $joins = array();
     foreach ($this->applicationSearchConstraints as $key => $constraint) {
       $table = $constraint['table'];
       $alias = 'appsearch_'.$key;
       $index = $constraint['index'];
       $cond = $constraint['cond'];
       $phid_column = $this->getApplicationSearchObjectPHIDColumn();
       switch ($cond) {
         case '=':
           $type = $constraint['type'];
           switch ($type) {
             case 'string':
               $constraint_clause = qsprintf(
                 $conn_r,
                 '%T.indexValue IN (%Ls)',
                 $alias,
                 (array)$constraint['value']);
               break;
             case 'int':
               $constraint_clause = qsprintf(
                 $conn_r,
                 '%T.indexValue IN (%Ld)',
                 $alias,
                 (array)$constraint['value']);
               break;
             default:
               throw new Exception(pht('Unknown index type "%s"!', $type));
           }
 
           $joins[] = qsprintf(
             $conn_r,
             'JOIN %T %T ON %T.objectPHID = %Q
               AND %T.indexKey = %s
               AND (%Q)',
             $table,
             $alias,
             $alias,
             $phid_column,
             $alias,
             $index,
             $constraint_clause);
           break;
         case 'range':
           list($min, $max) = $constraint['value'];
           if (($min === null) && ($max === null)) {
             // If there's no actual range constraint, just move on.
             break;
           }
 
           if ($min === null) {
             $constraint_clause = qsprintf(
               $conn_r,
               '%T.indexValue <= %d',
               $alias,
               $max);
           } else if ($max === null) {
             $constraint_clause = qsprintf(
               $conn_r,
               '%T.indexValue >= %d',
               $alias,
               $min);
           } else {
             $constraint_clause = qsprintf(
               $conn_r,
               '%T.indexValue BETWEEN %d AND %d',
               $alias,
               $min,
               $max);
           }
 
           $joins[] = qsprintf(
             $conn_r,
             'JOIN %T %T ON %T.objectPHID = %Q
               AND %T.indexKey = %s
               AND (%Q)',
             $table,
             $alias,
             $alias,
             $phid_column,
             $alias,
             $index,
             $constraint_clause);
           break;
         default:
           throw new Exception(pht('Unknown constraint condition "%s"!', $cond));
       }
     }
 
     $phid_column = $this->getApplicationSearchObjectPHIDColumn();
     $orderable = $this->getOrderableColumns();
 
     $vector = $this->getOrderVector();
     foreach ($vector as $order) {
       $spec = $orderable[$order->getOrderKey()];
       if (empty($spec['customfield'])) {
         continue;
       }
 
       $table = $spec['customfield.index.table'];
       $alias = $spec['table'];
       $key = $spec['customfield.index.key'];
 
       $joins[] = qsprintf(
         $conn_r,
         'LEFT JOIN %T %T ON %T.objectPHID = %Q
           AND %T.indexKey = %s',
         $table,
         $alias,
         $alias,
         $phid_column,
         $alias,
         $key);
     }
 
     return implode(' ', $joins);
   }
 
 
 /* -(  Integration with CustomField  )--------------------------------------- */
 
 
   /**
    * @task customfield
    */
   protected function getPagingValueMapForCustomFields(
     PhabricatorCustomFieldInterface $object) {
 
     // We have to get the current field values on the cursor object.
     $fields = PhabricatorCustomField::getObjectFields(
       $object,
       PhabricatorCustomField::ROLE_APPLICATIONSEARCH);
     $fields->setViewer($this->getViewer());
     $fields->readFieldsFromStorage($object);
 
     $map = array();
     foreach ($fields->getFields() as $field) {
       $map['custom:'.$field->getFieldKey()] = $field->getValueForStorage();
     }
 
     return $map;
   }
 
 
   /**
    * @task customfield
    */
   protected function isCustomFieldOrderKey($key) {
     $prefix = 'custom:';
     return !strncmp($key, $prefix, strlen($prefix));
   }
 
 
 /* -(  Edge Logic  )--------------------------------------------------------- */
 
 
   /**
    * Convenience method for specifying edge logic constraints with a list of
    * PHIDs.
    *
    * @param const Edge constant.
    * @param const Constraint operator.
    * @param list<phid> List of PHIDs.
    * @return this
    * @task edgelogic
    */
   public function withEdgeLogicPHIDs($edge_type, $operator, array $phids) {
     $constraints = array();
     foreach ($phids as $phid) {
       $constraints[] = new PhabricatorQueryConstraint($operator, $phid);
     }
 
     return $this->withEdgeLogicConstraints($edge_type, $constraints);
   }
 
 
   /**
    * @task edgelogic
    */
   public function withEdgeLogicConstraints($edge_type, array $constraints) {
     assert_instances_of($constraints, 'PhabricatorQueryConstraint');
 
     $constraints = mgroup($constraints, 'getOperator');
     foreach ($constraints as $operator => $list) {
       foreach ($list as $item) {
         $value = $item->getValue();
         $this->edgeLogicConstraints[$edge_type][$operator][$value] = $item;
       }
     }
 
     $this->edgeLogicConstraintsAreValid = false;
 
     return $this;
   }
 
 
   /**
    * @task edgelogic
    */
   public function buildEdgeLogicSelectClause(AphrontDatabaseConnection $conn) {
     $select = array();
 
     $this->validateEdgeLogicConstraints();
 
     foreach ($this->edgeLogicConstraints as $type => $constraints) {
       foreach ($constraints as $operator => $list) {
         $alias = $this->getEdgeLogicTableAlias($operator, $type);
         switch ($operator) {
           case PhabricatorQueryConstraint::OPERATOR_AND:
             if (count($list) > 1) {
               $select[] = qsprintf(
                 $conn,
                 'COUNT(DISTINCT(%T.dst)) %T',
                 $alias,
                 $this->buildEdgeLogicTableAliasCount($alias));
             }
             break;
           default:
             break;
         }
       }
     }
 
     return $select;
   }
 
 
   /**
    * @task edgelogic
    */
   public function buildEdgeLogicJoinClause(AphrontDatabaseConnection $conn) {
     $edge_table = PhabricatorEdgeConfig::TABLE_NAME_EDGE;
     $phid_column = $this->getApplicationSearchObjectPHIDColumn();
 
     $joins = array();
     foreach ($this->edgeLogicConstraints as $type => $constraints) {
 
       $op_null = PhabricatorQueryConstraint::OPERATOR_NULL;
       $has_null = isset($constraints[$op_null]);
 
       foreach ($constraints as $operator => $list) {
         $alias = $this->getEdgeLogicTableAlias($operator, $type);
         switch ($operator) {
           case PhabricatorQueryConstraint::OPERATOR_NOT:
             $joins[] = qsprintf(
               $conn,
               'LEFT JOIN %T %T ON %Q = %T.src AND %T.type = %d
                 AND %T.dst IN (%Ls)',
               $edge_table,
               $alias,
               $phid_column,
               $alias,
               $alias,
               $type,
               $alias,
               mpull($list, 'getValue'));
             break;
           case PhabricatorQueryConstraint::OPERATOR_AND:
           case PhabricatorQueryConstraint::OPERATOR_OR:
             // If we're including results with no matches, we have to degrade
             // this to a LEFT join. We'll use WHERE to select matching rows
             // later.
             if ($has_null) {
               $join_type = 'LEFT';
             } else {
               $join_type = '';
             }
 
             $joins[] = qsprintf(
               $conn,
               '%Q JOIN %T %T ON %Q = %T.src AND %T.type = %d
                 AND %T.dst IN (%Ls)',
               $join_type,
               $edge_table,
               $alias,
               $phid_column,
               $alias,
               $alias,
               $type,
               $alias,
               mpull($list, 'getValue'));
             break;
           case PhabricatorQueryConstraint::OPERATOR_NULL:
             $joins[] = qsprintf(
               $conn,
               'LEFT JOIN %T %T ON %Q = %T.src AND %T.type = %d',
               $edge_table,
               $alias,
               $phid_column,
               $alias,
               $alias,
               $type);
             break;
         }
       }
     }
 
     return $joins;
   }
 
 
   /**
    * @task edgelogic
    */
   public function buildEdgeLogicWhereClause(AphrontDatabaseConnection $conn) {
     $where = array();
 
     foreach ($this->edgeLogicConstraints as $type => $constraints) {
 
       $full = array();
       $null = array();
 
       $op_null = PhabricatorQueryConstraint::OPERATOR_NULL;
       $has_null = isset($constraints[$op_null]);
 
       foreach ($constraints as $operator => $list) {
         $alias = $this->getEdgeLogicTableAlias($operator, $type);
         switch ($operator) {
           case PhabricatorQueryConstraint::OPERATOR_NOT:
             $full[] = qsprintf(
               $conn,
               '%T.dst IS NULL',
               $alias);
             break;
           case PhabricatorQueryConstraint::OPERATOR_AND:
           case PhabricatorQueryConstraint::OPERATOR_OR:
             if ($has_null) {
               $full[] = qsprintf(
                 $conn,
                 '%T.dst IS NOT NULL',
                 $alias);
             }
             break;
           case PhabricatorQueryConstraint::OPERATOR_NULL:
             $null[] = qsprintf(
               $conn,
               '%T.dst IS NULL',
               $alias);
             break;
         }
       }
 
       if ($full && $null) {
         $full = $this->formatWhereSubclause($full);
         $null = $this->formatWhereSubclause($null);
         $where[] = qsprintf($conn, '(%Q OR %Q)', $full, $null);
       } else if ($full) {
         foreach ($full as $condition) {
           $where[] = $condition;
         }
       } else if ($null) {
         foreach ($null as $condition) {
           $where[] = $condition;
         }
       }
     }
 
     return $where;
   }
 
 
   /**
    * @task edgelogic
    */
   public function buildEdgeLogicHavingClause(AphrontDatabaseConnection $conn) {
     $having = array();
 
     foreach ($this->edgeLogicConstraints as $type => $constraints) {
       foreach ($constraints as $operator => $list) {
         $alias = $this->getEdgeLogicTableAlias($operator, $type);
         switch ($operator) {
           case PhabricatorQueryConstraint::OPERATOR_AND:
             if (count($list) > 1) {
               $having[] = qsprintf(
                 $conn,
                 '%T = %d',
                 $this->buildEdgeLogicTableAliasCount($alias),
                 count($list));
             }
             break;
         }
       }
     }
 
     return $having;
   }
 
 
   /**
    * @task edgelogic
    */
   public function shouldGroupEdgeLogicResultRows() {
     foreach ($this->edgeLogicConstraints as $type => $constraints) {
       foreach ($constraints as $operator => $list) {
         switch ($operator) {
           case PhabricatorQueryConstraint::OPERATOR_NOT:
           case PhabricatorQueryConstraint::OPERATOR_AND:
           case PhabricatorQueryConstraint::OPERATOR_OR:
             if (count($list) > 1) {
               return true;
             }
             break;
           case PhabricatorQueryConstraint::OPERATOR_NULL:
             return true;
         }
       }
     }
 
     return false;
   }
 
 
   /**
    * @task edgelogic
    */
   private function getEdgeLogicTableAlias($operator, $type) {
     return 'edgelogic_'.$operator.'_'.$type;
   }
 
 
   /**
    * @task edgelogic
    */
   private function buildEdgeLogicTableAliasCount($alias) {
     return $alias.'_count';
   }
 
 
   /**
    * Select certain edge logic constraint values.
    *
    * @task edgelogic
    */
   protected function getEdgeLogicValues(
     array $edge_types,
     array $operators) {
 
     $values = array();
 
     $constraint_lists = $this->edgeLogicConstraints;
     if ($edge_types) {
       $constraint_lists = array_select_keys($constraint_lists, $edge_types);
     }
 
     foreach ($constraint_lists as $type => $constraints) {
       if ($operators) {
         $constraints = array_select_keys($constraints, $operators);
       }
       foreach ($constraints as $operator => $list) {
         foreach ($list as $constraint) {
           $values[] = $constraint->getValue();
         }
       }
     }
 
     return $values;
   }
 
 
   /**
    * Validate edge logic constraints for the query.
    *
    * @return this
    * @task edgelogic
    */
   private function validateEdgeLogicConstraints() {
     if ($this->edgeLogicConstraintsAreValid) {
       return $this;
     }
 
     // This should probably be more modular, eventually, but we only do
     // project-based edge logic today.
 
     $project_phids = $this->getEdgeLogicValues(
       array(
         PhabricatorProjectObjectHasProjectEdgeType::EDGECONST,
       ),
       array(
         PhabricatorQueryConstraint::OPERATOR_AND,
         PhabricatorQueryConstraint::OPERATOR_OR,
         PhabricatorQueryConstraint::OPERATOR_NOT,
       ));
     if ($project_phids) {
       $projects = id(new PhabricatorProjectQuery())
         ->setViewer($this->getViewer())
         ->setParentQuery($this)
         ->withPHIDs($project_phids)
         ->execute();
       $projects = mpull($projects, null, 'getPHID');
       foreach ($project_phids as $phid) {
         if (empty($projects[$phid])) {
           throw new PhabricatorEmptyQueryException(
             pht(
               'This query is constrained by a project you do not have '.
               'permission to see.'));
         }
       }
     }
 
     $this->edgeLogicConstraintsAreValid = true;
 
     return $this;
   }
 
 
 /* -(  Spaces  )------------------------------------------------------------- */
 
 
   /**
    * Constrain the query to return results from only specific Spaces.
    *
    * Pass a list of Space PHIDs, or `null` to represent the default space. Only
    * results in those Spaces will be returned.
    *
    * Queries are always constrained to include only results from spaces the
    * viewer has access to.
    *
    * @param list<phid|null>
    * @task spaces
    */
   public function withSpacePHIDs(array $space_phids) {
     $object = $this->newResultObject();
 
     if (!$object) {
       throw new Exception(
         pht(
           'This query (of class "%s") does not implement newResultObject(), '.
           'but must implement this method to enable support for Spaces.',
           get_class($this)));
     }
 
     if (!($object instanceof PhabricatorSpacesInterface)) {
       throw new Exception(
         pht(
           'This query (of class "%s") returned an object of class "%s" from '.
           'getNewResultObject(), but it does not implement the required '.
           'interface ("%s"). Objects must implement this interface to enable '.
           'Spaces support.',
           get_class($this),
           get_class($object),
           'PhabricatorSpacesInterface'));
     }
 
     $this->spacePHIDs = $space_phids;
 
     return $this;
   }
 
+  public function withSpaceIsArchived($archived) {
+    $this->spaceIsArchived = $archived;
+    return $this;
+  }
+
 
   /**
    * Constrain the query to include only results in valid Spaces.
    *
    * This method builds part of a WHERE clause which considers the spaces the
    * viewer has access to see with any explicit constraint on spaces added by
    * @{method:withSpacePHIDs}.
    *
    * @param AphrontDatabaseConnection Database connection.
    * @return string Part of a WHERE clause.
    * @task spaces
    */
   private function buildSpacesWhereClause(AphrontDatabaseConnection $conn) {
     $object = $this->newResultObject();
     if (!$object) {
       return null;
     }
 
     if (!($object instanceof PhabricatorSpacesInterface)) {
       return null;
     }
 
     $viewer = $this->getViewer();
 
     $space_phids = array();
     $include_null = false;
 
     $all = PhabricatorSpacesNamespaceQuery::getAllSpaces();
     if (!$all) {
       // If there are no spaces at all, implicitly give the viewer access to
       // the default space.
       $include_null = true;
     } else {
       // Otherwise, give them access to the spaces they have permission to
       // see.
       $viewer_spaces = PhabricatorSpacesNamespaceQuery::getViewerSpaces(
         $viewer);
       foreach ($viewer_spaces as $viewer_space) {
+        if ($this->spaceIsArchived !== null) {
+          if ($viewer_space->getIsArchived() != $this->spaceIsArchived) {
+            continue;
+          }
+        }
         $phid = $viewer_space->getPHID();
         $space_phids[$phid] = $phid;
         if ($viewer_space->getIsDefaultNamespace()) {
           $include_null = true;
         }
       }
     }
 
     // If we have additional explicit constraints, evaluate them now.
     if ($this->spacePHIDs !== null) {
       $explicit = array();
       $explicit_null = false;
       foreach ($this->spacePHIDs as $phid) {
         if ($phid === null) {
           $space = PhabricatorSpacesNamespaceQuery::getDefaultSpace();
         } else {
           $space = idx($all, $phid);
         }
 
         if ($space) {
           $phid = $space->getPHID();
           $explicit[$phid] = $phid;
           if ($space->getIsDefaultNamespace()) {
             $explicit_null = true;
           }
         }
       }
 
       // If the viewer can see the default space but it isn't on the explicit
       // list of spaces to query, don't match it.
       if ($include_null && !$explicit_null) {
         $include_null = false;
       }
 
       // Include only the spaces common to the viewer and the constraints.
       $space_phids = array_intersect_key($space_phids, $explicit);
     }
 
     if (!$space_phids && !$include_null) {
       if ($this->spacePHIDs === null) {
         throw new PhabricatorEmptyQueryException(
           pht('You do not have access to any spaces.'));
       } else {
         throw new PhabricatorEmptyQueryException(
           pht(
             'You do not have access to any of the spaces this query '.
             'is constrained to.'));
       }
     }
 
     $alias = $this->getPrimaryTableAlias();
     if ($alias) {
       $col = qsprintf($conn, '%T.spacePHID', $alias);
     } else {
       $col = 'spacePHID';
     }
 
     if ($space_phids && $include_null) {
       return qsprintf(
         $conn,
         '(%Q IN (%Ls) OR %Q IS NULL)',
         $col,
         $space_phids,
         $col);
     } else if ($space_phids) {
       return qsprintf(
         $conn,
         '%Q IN (%Ls)',
         $col,
         $space_phids);
     } else {
       return qsprintf(
         $conn,
         '%Q IS NULL',
         $col);
     }
   }
 
 }
diff --git a/src/infrastructure/query/policy/PhabricatorPolicyAwareQuery.php b/src/infrastructure/query/policy/PhabricatorPolicyAwareQuery.php
index 1d8f10b91..e7b9b3562 100644
--- a/src/infrastructure/query/policy/PhabricatorPolicyAwareQuery.php
+++ b/src/infrastructure/query/policy/PhabricatorPolicyAwareQuery.php
@@ -1,683 +1,699 @@
 <?php
 
 /**
  * A @{class:PhabricatorQuery} which filters results according to visibility
  * policies for the querying user. Broadly, this class allows you to implement
  * a query that returns only objects the user is allowed to see.
  *
  *   $results = id(new ExampleQuery())
  *     ->setViewer($user)
  *     ->withConstraint($example)
  *     ->execute();
  *
  * Normally, you should extend @{class:PhabricatorCursorPagedPolicyAwareQuery},
  * not this class. @{class:PhabricatorCursorPagedPolicyAwareQuery} provides a
  * more practical interface for building usable queries against most object
  * types.
  *
  * NOTE: Although this class extends @{class:PhabricatorOffsetPagedQuery},
  * offset paging with policy filtering is not efficient. All results must be
  * loaded into the application and filtered here: skipping `N` rows via offset
  * is an `O(N)` operation with a large constant. Prefer cursor-based paging
  * with @{class:PhabricatorCursorPagedPolicyAwareQuery}, which can filter far
  * more efficiently in MySQL.
  *
  * @task config     Query Configuration
  * @task exec       Executing Queries
  * @task policyimpl Policy Query Implementation
  */
 abstract class PhabricatorPolicyAwareQuery extends PhabricatorOffsetPagedQuery {
 
   private $viewer;
   private $parentQuery;
   private $rawResultLimit;
   private $capabilities;
   private $workspace = array();
   private $inFlightPHIDs = array();
   private $policyFilteredPHIDs = array();
   private $canUseApplication;
 
   /**
    * Should we continue or throw an exception when a query result is filtered
    * by policy rules?
    *
    * Values are `true` (raise exceptions), `false` (do not raise exceptions)
    * and `null` (inherit from parent query, with no exceptions by default).
    */
   private $raisePolicyExceptions;
 
 
 /* -(  Query Configuration  )------------------------------------------------ */
 
 
   /**
    * Set the viewer who is executing the query. Results will be filtered
    * according to the viewer's capabilities. You must set a viewer to execute
    * a policy query.
    *
    * @param PhabricatorUser The viewing user.
    * @return this
    * @task config
    */
   final public function setViewer(PhabricatorUser $viewer) {
     $this->viewer = $viewer;
     return $this;
   }
 
 
   /**
    * Get the query's viewer.
    *
    * @return PhabricatorUser The viewing user.
    * @task config
    */
   final public function getViewer() {
     return $this->viewer;
   }
 
 
   /**
    * Set the parent query of this query. This is useful for nested queries so
    * that configuration like whether or not to raise policy exceptions is
    * seamlessly passed along to child queries.
    *
    * @return this
    * @task config
    */
   final public function setParentQuery(PhabricatorPolicyAwareQuery $query) {
     $this->parentQuery = $query;
     return $this;
   }
 
 
   /**
    * Get the parent query. See @{method:setParentQuery} for discussion.
    *
    * @return PhabricatorPolicyAwareQuery The parent query.
    * @task config
    */
   final public function getParentQuery() {
     return $this->parentQuery;
   }
 
 
   /**
    * Hook to configure whether this query should raise policy exceptions.
    *
    * @return this
    * @task config
    */
   final public function setRaisePolicyExceptions($bool) {
     $this->raisePolicyExceptions = $bool;
     return $this;
   }
 
 
   /**
    * @return bool
    * @task config
    */
   final public function shouldRaisePolicyExceptions() {
     return (bool)$this->raisePolicyExceptions;
   }
 
 
   /**
    * @task config
    */
   final public function requireCapabilities(array $capabilities) {
     $this->capabilities = $capabilities;
     return $this;
   }
 
 
 /* -(  Query Execution  )---------------------------------------------------- */
 
 
   /**
    * Execute the query, expecting a single result. This method simplifies
    * loading objects for detail pages or edit views.
    *
    *   // Load one result by ID.
    *   $obj = id(new ExampleQuery())
    *     ->setViewer($user)
    *     ->withIDs(array($id))
    *     ->executeOne();
    *   if (!$obj) {
    *     return new Aphront404Response();
    *   }
    *
    * If zero results match the query, this method returns `null`.
    * If one result matches the query, this method returns that result.
    *
    * If two or more results match the query, this method throws an exception.
    * You should use this method only when the query constraints guarantee at
    * most one match (e.g., selecting a specific ID or PHID).
    *
    * If one result matches the query but it is caught by the policy filter (for
    * example, the user is trying to view or edit an object which exists but
    * which they do not have permission to see) a policy exception is thrown.
    *
    * @return mixed Single result, or null.
    * @task exec
    */
   final public function executeOne() {
 
     $this->setRaisePolicyExceptions(true);
     try {
       $results = $this->execute();
     } catch (Exception $ex) {
       $this->setRaisePolicyExceptions(false);
       throw $ex;
     }
 
     if (count($results) > 1) {
       throw new Exception(pht('Expected a single result!'));
     }
 
     if (!$results) {
       return null;
     }
 
     return head($results);
   }
 
 
   /**
    * Execute the query, loading all visible results.
    *
    * @return list<PhabricatorPolicyInterface> Result objects.
    * @task exec
    */
   final public function execute() {
     if (!$this->viewer) {
       throw new PhutilInvalidStateException('setViewer');
     }
 
     $parent_query = $this->getParentQuery();
     if ($parent_query && ($this->raisePolicyExceptions === null)) {
       $this->setRaisePolicyExceptions(
         $parent_query->shouldRaisePolicyExceptions());
     }
 
     $results = array();
 
     $filter = $this->getPolicyFilter();
 
     $offset = (int)$this->getOffset();
     $limit  = (int)$this->getLimit();
     $count  = 0;
 
     if ($limit) {
       $need = $offset + $limit;
     } else {
       $need = 0;
     }
 
     $this->willExecute();
 
     do {
       if ($need) {
         $this->rawResultLimit = min($need - $count, 1024);
       } else {
         $this->rawResultLimit = 0;
       }
 
       if ($this->canViewerUseQueryApplication()) {
         try {
           $page = $this->loadPage();
         } catch (PhabricatorEmptyQueryException $ex) {
           $page = array();
         }
       } else {
         $page = array();
       }
 
       if ($page) {
         $maybe_visible = $this->willFilterPage($page);
       } else {
         $maybe_visible = array();
       }
 
       if ($this->shouldDisablePolicyFiltering()) {
         $visible = $maybe_visible;
       } else {
         $visible = $filter->apply($maybe_visible);
 
         $policy_filtered = array();
         foreach ($maybe_visible as $key => $object) {
           if (empty($visible[$key])) {
             $phid = $object->getPHID();
             if ($phid) {
               $policy_filtered[$phid] = $phid;
             }
           }
         }
         $this->addPolicyFilteredPHIDs($policy_filtered);
       }
 
       if ($visible) {
         $this->putObjectsInWorkspace($this->getWorkspaceMapForPage($visible));
         $visible = $this->didFilterPage($visible);
       }
 
       $removed = array();
       foreach ($maybe_visible as $key => $object) {
         if (empty($visible[$key])) {
           $removed[$key] = $object;
         }
       }
 
       $this->didFilterResults($removed);
 
       foreach ($visible as $key => $result) {
         ++$count;
 
         // If we have an offset, we just ignore that many results and start
         // storing them only once we've hit the offset. This reduces memory
         // requirements for large offsets, compared to storing them all and
         // slicing them away later.
         if ($count > $offset) {
           $results[$key] = $result;
         }
 
         if ($need && ($count >= $need)) {
           // If we have all the rows we need, break out of the paging query.
           break 2;
         }
       }
 
       if (!$this->rawResultLimit) {
         // If we don't have a load count, we loaded all the results. We do
         // not need to load another page.
         break;
       }
 
       if (count($page) < $this->rawResultLimit) {
         // If we have a load count but the unfiltered results contained fewer
         // objects, we know this was the last page of objects; we do not need
         // to load another page because we can deduce it would be empty.
         break;
       }
 
       $this->nextPage($page);
     } while (true);
 
     $results = $this->didLoadResults($results);
 
     return $results;
   }
 
   private function getPolicyFilter() {
     $filter = new PhabricatorPolicyFilter();
     $filter->setViewer($this->viewer);
     $capabilities = $this->getRequiredCapabilities();
     $filter->requireCapabilities($capabilities);
     $filter->raisePolicyExceptions($this->shouldRaisePolicyExceptions());
 
     return $filter;
   }
 
   protected function getRequiredCapabilities() {
     if ($this->capabilities) {
       return $this->capabilities;
     }
 
     return array(
       PhabricatorPolicyCapability::CAN_VIEW,
     );
   }
 
   protected function applyPolicyFilter(array $objects, array $capabilities) {
     if ($this->shouldDisablePolicyFiltering()) {
       return $objects;
     }
     $filter = $this->getPolicyFilter();
     $filter->requireCapabilities($capabilities);
     return $filter->apply($objects);
   }
 
   protected function didRejectResult(PhabricatorPolicyInterface $object) {
+    // Some objects (like commits) may be rejected because related objects
+    // (like repositories) can not be loaded. In some cases, we may need these
+    // related objects to determine the object policy, so it's expected that
+    // we may occasionally be unable to determine the policy.
+
+    try {
+      $policy = $object->getPolicy(PhabricatorPolicyCapability::CAN_VIEW);
+    } catch (Exception $ex) {
+      $policy = null;
+    }
+
+    // Mark this object as filtered so handles can render "Restricted" instead
+    // of "Unknown".
+    $phid = $object->getPHID();
+    $this->addPolicyFilteredPHIDs(array($phid => $phid));
+
     $this->getPolicyFilter()->rejectObject(
       $object,
-      $object->getPolicy(PhabricatorPolicyCapability::CAN_VIEW),
+      $policy,
       PhabricatorPolicyCapability::CAN_VIEW);
   }
 
   public function addPolicyFilteredPHIDs(array $phids) {
     $this->policyFilteredPHIDs += $phids;
     if ($this->getParentQuery()) {
       $this->getParentQuery()->addPolicyFilteredPHIDs($phids);
     }
     return $this;
   }
 
   /**
    * Return a map of all object PHIDs which were loaded in the query but
    * filtered out by policy constraints. This allows a caller to distinguish
    * between objects which do not exist (or, at least, were filtered at the
    * content level) and objects which exist but aren't visible.
    *
    * @return map<phid, phid> Map of object PHIDs which were filtered
    *   by policies.
    * @task exec
    */
   public function getPolicyFilteredPHIDs() {
     return $this->policyFilteredPHIDs;
   }
 
 
 /* -(  Query Workspace  )---------------------------------------------------- */
 
 
   /**
    * Put a map of objects into the query workspace. Many queries perform
    * subqueries, which can eventually end up loading the same objects more than
    * once (often to perform policy checks).
    *
    * For example, loading a user may load the user's profile image, which might
    * load the user object again in order to verify that the viewer has
    * permission to see the file.
    *
    * The "query workspace" allows queries to load objects from elsewhere in a
    * query block instead of refetching them.
    *
    * When using the query workspace, it's important to obey two rules:
    *
    * **Never put objects into the workspace which the viewer may not be able
    * to see**. You need to apply all policy filtering //before// putting
    * objects in the workspace. Otherwise, subqueries may read the objects and
    * use them to permit access to content the user shouldn't be able to view.
    *
    * **Fully enrich objects pulled from the workspace.** After pulling objects
    * from the workspace, you still need to load and attach any additional
    * content the query requests. Otherwise, a query might return objects without
    * requested content.
    *
    * Generally, you do not need to update the workspace yourself: it is
    * automatically populated as a side effect of objects surviving policy
    * filtering.
    *
    * @param map<phid, PhabricatorPolicyInterface> Objects to add to the query
    *   workspace.
    * @return this
    * @task workspace
    */
   public function putObjectsInWorkspace(array $objects) {
     assert_instances_of($objects, 'PhabricatorPolicyInterface');
 
     $viewer_phid = $this->getViewer()->getPHID();
 
     // The workspace is scoped per viewer to prevent accidental contamination.
     if (empty($this->workspace[$viewer_phid])) {
       $this->workspace[$viewer_phid] = array();
     }
 
     $this->workspace[$viewer_phid] += $objects;
 
     return $this;
   }
 
 
   /**
    * Retrieve objects from the query workspace. For more discussion about the
    * workspace mechanism, see @{method:putObjectsInWorkspace}. This method
    * searches both the current query's workspace and the workspaces of parent
    * queries.
    *
    * @param list<phid> List of PHIDs to retrieve.
    * @return this
    * @task workspace
    */
   public function getObjectsFromWorkspace(array $phids) {
     $viewer_phid = $this->getViewer()->getPHID();
 
     $results = array();
     foreach ($phids as $key => $phid) {
       if (isset($this->workspace[$viewer_phid][$phid])) {
         $results[$phid] = $this->workspace[$viewer_phid][$phid];
         unset($phids[$key]);
       }
     }
 
     if ($phids && $this->getParentQuery()) {
       $results += $this->getParentQuery()->getObjectsFromWorkspace($phids);
     }
 
     return $results;
   }
 
 
   /**
    * Convert a result page to a `<phid, PhabricatorPolicyInterface>` map.
    *
    * @param list<PhabricatorPolicyInterface> Objects.
    * @return map<phid, PhabricatorPolicyInterface> Map of objects which can
    *   be put into the workspace.
    * @task workspace
    */
   protected function getWorkspaceMapForPage(array $results) {
     $map = array();
     foreach ($results as $result) {
       $phid = $result->getPHID();
       if ($phid !== null) {
         $map[$phid] = $result;
       }
     }
 
     return $map;
   }
 
 
   /**
    * Mark PHIDs as in flight.
    *
    * PHIDs which are "in flight" are actively being queried for. Using this
    * list can prevent infinite query loops by aborting queries which cycle.
    *
    * @param list<phid> List of PHIDs which are now in flight.
    * @return this
    */
   public function putPHIDsInFlight(array $phids) {
     foreach ($phids as $phid) {
       $this->inFlightPHIDs[$phid] = $phid;
     }
     return $this;
   }
 
 
   /**
    * Get PHIDs which are currently in flight.
    *
    * PHIDs which are "in flight" are actively being queried for.
    *
    * @return map<phid, phid> PHIDs currently in flight.
    */
   public function getPHIDsInFlight() {
     $results = $this->inFlightPHIDs;
     if ($this->getParentQuery()) {
       $results += $this->getParentQuery()->getPHIDsInFlight();
     }
     return $results;
   }
 
 
 /* -(  Policy Query Implementation  )---------------------------------------- */
 
 
   /**
    * Get the number of results @{method:loadPage} should load. If the value is
    * 0, @{method:loadPage} should load all available results.
    *
    * @return int The number of results to load, or 0 for all results.
    * @task policyimpl
    */
   final protected function getRawResultLimit() {
     return $this->rawResultLimit;
   }
 
 
   /**
    * Hook invoked before query execution. Generally, implementations should
    * reset any internal cursors.
    *
    * @return void
    * @task policyimpl
    */
   protected function willExecute() {
     return;
   }
 
 
   /**
    * Load a raw page of results. Generally, implementations should load objects
    * from the database. They should attempt to return the number of results
    * hinted by @{method:getRawResultLimit}.
    *
    * @return list<PhabricatorPolicyInterface> List of filterable policy objects.
    * @task policyimpl
    */
   abstract protected function loadPage();
 
 
   /**
    * Update internal state so that the next call to @{method:loadPage} will
    * return new results. Generally, you should adjust a cursor position based
    * on the provided result page.
    *
    * @param list<PhabricatorPolicyInterface> The current page of results.
    * @return void
    * @task policyimpl
    */
   abstract protected function nextPage(array $page);
 
 
   /**
    * Hook for applying a page filter prior to the privacy filter. This allows
    * you to drop some items from the result set without creating problems with
    * pagination or cursor updates. You can also load and attach data which is
    * required to perform policy filtering.
    *
    * Generally, you should load non-policy data and perform non-policy filtering
    * later, in @{method:didFilterPage}. Strictly fewer objects will make it that
    * far (so the program will load less data) and subqueries from that context
    * can use the query workspace to further reduce query load.
    *
    * This method will only be called if data is available. Implementations
    * do not need to handle the case of no results specially.
    *
    * @param   list<wild>  Results from `loadPage()`.
    * @return  list<PhabricatorPolicyInterface> Objects for policy filtering.
    * @task policyimpl
    */
   protected function willFilterPage(array $page) {
     return $page;
   }
 
   /**
    * Hook for performing additional non-policy loading or filtering after an
    * object has satisfied all policy checks. Generally, this means loading and
    * attaching related data.
    *
    * Subqueries executed during this phase can use the query workspace, which
    * may improve performance or make circular policies resolvable. Data which
    * is not necessary for policy filtering should generally be loaded here.
    *
    * This callback can still filter objects (for example, if attachable data
    * is discovered to not exist), but should not do so for policy reasons.
    *
    * This method will only be called if data is available. Implementations do
    * not need to handle the case of no results specially.
    *
    * @param list<wild> Results from @{method:willFilterPage()}.
    * @return list<PhabricatorPolicyInterface> Objects after additional
    *   non-policy processing.
    */
   protected function didFilterPage(array $page) {
     return $page;
   }
 
 
   /**
    * Hook for removing filtered results from alternate result sets. This
    * hook will be called with any objects which were returned by the query but
    * filtered for policy reasons. The query should remove them from any cached
    * or partial result sets.
    *
    * @param list<wild>  List of objects that should not be returned by alternate
    *                    result mechanisms.
    * @return void
    * @task policyimpl
    */
   protected function didFilterResults(array $results) {
     return;
   }
 
 
   /**
    * Hook for applying final adjustments before results are returned. This is
    * used by @{class:PhabricatorCursorPagedPolicyAwareQuery} to reverse results
    * that are queried during reverse paging.
    *
    * @param   list<PhabricatorPolicyInterface> Query results.
    * @return  list<PhabricatorPolicyInterface> Final results.
    * @task policyimpl
    */
   protected function didLoadResults(array $results) {
     return $results;
   }
 
 
   /**
    * Allows a subclass to disable policy filtering. This method is dangerous.
    * It should be used only if the query loads data which has already been
    * filtered (for example, because it wraps some other query which uses
    * normal policy filtering).
    *
    * @return bool True to disable all policy filtering.
    * @task policyimpl
    */
   protected function shouldDisablePolicyFiltering() {
     return false;
   }
 
 
   /**
    * If this query belongs to an application, return the application class name
    * here. This will prevent the query from returning results if the viewer can
    * not access the application.
    *
    * If this query does not belong to an application, return `null`.
    *
    * @return string|null Application class name.
    */
   abstract public function getQueryApplicationClass();
 
 
   /**
    * Determine if the viewer has permission to use this query's application.
    * For queries which aren't part of an application, this method always returns
    * true.
    *
    * @return bool True if the viewer has application-level permission to
    *   execute the query.
    */
   public function canViewerUseQueryApplication() {
     if ($this->canUseApplication === null) {
       $class = $this->getQueryApplicationClass();
       if (!$class) {
         $this->canUseApplication = true;
       } else {
         $result = id(new PhabricatorApplicationQuery())
           ->setViewer($this->getViewer())
           ->withClasses(array($class))
           ->execute();
 
         $this->canUseApplication = (bool)$result;
       }
     }
 
     return $this->canUseApplication;
   }
 
 }
diff --git a/src/infrastructure/sms/adapter/PhabricatorSMSImplementationAdapter.php b/src/infrastructure/sms/adapter/PhabricatorSMSImplementationAdapter.php
index 339a224f8..84325774f 100644
--- a/src/infrastructure/sms/adapter/PhabricatorSMSImplementationAdapter.php
+++ b/src/infrastructure/sms/adapter/PhabricatorSMSImplementationAdapter.php
@@ -1,88 +1,88 @@
 <?php
 
-abstract class PhabricatorSMSImplementationAdapter {
+abstract class PhabricatorSMSImplementationAdapter extends Phobject {
 
   private $fromNumber;
   private $toNumber;
   private $body;
 
   public function setFrom($number) {
     $this->fromNumber = $number;
     return $this;
   }
 
   public function getFrom() {
     return $this->fromNumber;
   }
 
   public function setTo($number) {
     $this->toNumber = $number;
     return $this;
   }
 
   public function getTo() {
     return $this->toNumber;
   }
 
   public function setBody($body) {
     $this->body = $body;
     return $this;
   }
 
   public function getBody() {
     return $this->body;
   }
 
   /**
    * 16 characters or less, to be used in database columns and exposed
    * to administrators during configuration directly.
    */
   abstract public function getProviderShortName();
 
   /**
    * Send the message. Generally, this means connecting to some service and
    * handing data to it. SMS APIs are generally asynchronous, so truly
    * determining success or failure is probably impossible synchronously.
    *
    * That said, if the adapter determines that the SMS will never be
    * deliverable, or there is some other known failure, it should throw
    * an exception.
    *
    * @return null
    */
   abstract public function send();
 
   /**
    * Most (all?) SMS APIs are asynchronous, but some do send back some
    * initial information. Use this hook to determine what the updated
    * sentStatus should be and what the provider is using for an SMS ID,
    * as well as throw exceptions if there are any failures.
    *
    * @return array Tuple of ($sms_id and $sent_status)
    */
   abstract public function getSMSDataFromResult($result);
 
   /**
    * Due to the asynchronous nature of sending SMS messages, it can be
    * necessary to poll the provider regarding the sent status of a given
    * sms.
    *
    * For now, this *MUST* be implemented and *MUST* work.
    */
   abstract public function pollSMSSentStatus(PhabricatorSMS $sms);
 
   /**
    * Convenience function to handle sending an SMS.
    */
   public static function sendSMS(array $to_numbers, $body) {
     PhabricatorWorker::scheduleTask(
       'PhabricatorSMSDemultiplexWorker',
       array(
         'toNumbers'  => $to_numbers,
         'body'       => $body,
       ),
       array(
         'priority' => PhabricatorWorker::PRIORITY_ALERTS,
       ));
   }
 }
diff --git a/src/infrastructure/storage/configuration/DatabaseConfigurationProvider.php b/src/infrastructure/storage/configuration/DatabaseConfigurationProvider.php
index 9c12607ef..abd0c9fb4 100644
--- a/src/infrastructure/storage/configuration/DatabaseConfigurationProvider.php
+++ b/src/infrastructure/storage/configuration/DatabaseConfigurationProvider.php
@@ -1,19 +1,16 @@
 <?php
 
-/**
- * @stable
- */
 interface DatabaseConfigurationProvider {
 
   public function __construct(
     LiskDAO $dao = null,
     $mode = 'r',
     $namespace = 'phabricator');
 
   public function getUser();
   public function getPassword();
   public function getHost();
   public function getPort();
   public function getDatabase();
 
 }
diff --git a/src/infrastructure/storage/configuration/DefaultDatabaseConfigurationProvider.php b/src/infrastructure/storage/configuration/DefaultDatabaseConfigurationProvider.php
index ea83209b4..aef9b1684 100644
--- a/src/infrastructure/storage/configuration/DefaultDatabaseConfigurationProvider.php
+++ b/src/infrastructure/storage/configuration/DefaultDatabaseConfigurationProvider.php
@@ -1,47 +1,48 @@
 <?php
 
 final class DefaultDatabaseConfigurationProvider
+  extends Phobject
   implements DatabaseConfigurationProvider {
 
   private $dao;
   private $mode;
   private $namespace;
 
   public function __construct(
     LiskDAO $dao = null,
     $mode = 'r',
     $namespace = 'phabricator') {
 
     $this->dao = $dao;
     $this->mode = $mode;
     $this->namespace = $namespace;
   }
 
   public function getUser() {
     return PhabricatorEnv::getEnvConfig('mysql.user');
   }
 
   public function getPassword() {
     return new PhutilOpaqueEnvelope(PhabricatorEnv::getEnvConfig('mysql.pass'));
   }
 
   public function getHost() {
     return PhabricatorEnv::getEnvConfig('mysql.host');
   }
 
   public function getPort() {
     return PhabricatorEnv::getEnvConfig('mysql.port');
   }
 
   public function getDatabase() {
     if (!$this->getDao()) {
       return null;
     }
     return $this->namespace.'_'.$this->getDao()->getApplicationName();
   }
 
   protected function getDao() {
     return $this->dao;
   }
 
 }
diff --git a/src/infrastructure/storage/lisk/LiskDAO.php b/src/infrastructure/storage/lisk/LiskDAO.php
index 4e02d4226..d43536c91 100644
--- a/src/infrastructure/storage/lisk/LiskDAO.php
+++ b/src/infrastructure/storage/lisk/LiskDAO.php
@@ -1,1956 +1,1956 @@
 <?php
 
 /**
  * Simple object-authoritative data access object that makes it easy to build
  * stuff that you need to save to a database. Basically, it means that the
  * amount of boilerplate code (and, particularly, boilerplate SQL) you need
  * to write is greatly reduced.
  *
  * Lisk makes it fairly easy to build something quickly and end up with
  * reasonably high-quality code when you're done (e.g., getters and setters,
  * objects, transactions, reasonably structured OO code). It's also very thin:
  * you can break past it and use MySQL and other lower-level tools when you
  * need to in those couple of cases where it doesn't handle your workflow
  * gracefully.
  *
  * However, Lisk won't scale past one database and lacks many of the features
  * of modern DAOs like Hibernate: for instance, it does not support joins or
  * polymorphic storage.
  *
  * This means that Lisk is well-suited for tools like Differential, but often a
  * poor choice elsewhere. And it is strictly unsuitable for many projects.
  *
  * Lisk's model is object-authoritative: the PHP class definition is the
  * master authority for what the object looks like.
  *
  * =Building New Objects=
  *
  * To create new Lisk objects, extend @{class:LiskDAO} and implement
  * @{method:establishLiveConnection}. It should return an
  * @{class:AphrontDatabaseConnection}; this will tell Lisk where to save your
  * objects.
  *
  *   class Dog extends LiskDAO {
  *
  *     protected $name;
  *     protected $breed;
  *
  *     public function establishLiveConnection() {
  *       return $some_connection_object;
  *     }
  *   }
  *
  * Now, you should create your table:
  *
  *   lang=sql
  *   CREATE TABLE dog (
  *     id int unsigned not null auto_increment primary key,
  *     name varchar(32) not null,
  *     breed varchar(32) not null,
  *     dateCreated int unsigned not null,
  *     dateModified int unsigned not null
  *   );
  *
  * For each property in your class, add a column with the same name to the table
  * (see @{method:getConfiguration} for information about changing this mapping).
  * Additionally, you should create the three columns `id`,  `dateCreated` and
  * `dateModified`. Lisk will automatically manage these, using them to implement
  * autoincrement IDs and timestamps. If you do not want to use these features,
  * see @{method:getConfiguration} for information on disabling them. At a bare
  * minimum, you must normally have an `id` column which is a primary or unique
  * key with a numeric type, although you can change its name by overriding
  * @{method:getIDKey} or disable it entirely by overriding @{method:getIDKey} to
  * return null. Note that many methods rely on a single-part primary key and
  * will no longer work (they will throw) if you disable it.
  *
  * As you add more properties to your class in the future, remember to add them
  * to the database table as well.
  *
  * Lisk will now automatically handle these operations: getting and setting
  * properties, saving objects, loading individual objects, loading groups
  * of objects, updating objects, managing IDs, updating timestamps whenever
  * an object is created or modified, and some additional specialized
  * operations.
  *
  * = Creating, Retrieving, Updating, and Deleting =
  *
  * To create and persist a Lisk object, use @{method:save}:
  *
  *   $dog = id(new Dog())
  *     ->setName('Sawyer')
  *     ->setBreed('Pug')
  *     ->save();
  *
  * Note that **Lisk automatically builds getters and setters for all of your
  * object's protected properties** via @{method:__call}. If you want to add
  * custom behavior to your getters or setters, you can do so by overriding the
  * @{method:readField} and @{method:writeField} methods.
  *
  * Calling @{method:save} will persist the object to the database. After calling
  * @{method:save}, you can call @{method:getID} to retrieve the object's ID.
  *
  * To load objects by ID, use the @{method:load} method:
  *
  *   $dog = id(new Dog())->load($id);
  *
  * This will load the Dog record with ID $id into $dog, or `null` if no such
  * record exists (@{method:load} is an instance method rather than a static
  * method because PHP does not support late static binding, at least until PHP
  * 5.3).
  *
  * To update an object, change its properties and save it:
  *
  *   $dog->setBreed('Lab')->save();
  *
  * To delete an object, call @{method:delete}:
  *
  *   $dog->delete();
  *
  * That's Lisk CRUD in a nutshell.
  *
  * = Queries =
  *
  * Often, you want to load a bunch of objects, or execute a more specialized
  * query. Use @{method:loadAllWhere} or @{method:loadOneWhere} to do this:
  *
  *   $pugs = $dog->loadAllWhere('breed = %s', 'Pug');
  *   $sawyer = $dog->loadOneWhere('name = %s', 'Sawyer');
  *
  * These methods work like @{function@libphutil:queryfx}, but only take half of
  * a query (the part after the WHERE keyword). Lisk will handle the connection,
  * columns, and object construction; you are responsible for the rest of it.
  * @{method:loadAllWhere} returns a list of objects, while
  * @{method:loadOneWhere} returns a single object (or `null`).
  *
  * There's also a @{method:loadRelatives} method which helps to prevent the 1+N
  * queries problem.
  *
  * = Managing Transactions =
  *
  * Lisk uses a transaction stack, so code does not generally need to be aware
  * of the transactional state of objects to implement correct transaction
  * semantics:
  *
  *   $obj->openTransaction();
  *     $obj->save();
  *     $other->save();
  *     // ...
  *     $other->openTransaction();
  *       $other->save();
  *       $another->save();
  *     if ($some_condition) {
  *       $other->saveTransaction();
  *     } else {
  *       $other->killTransaction();
  *     }
  *     // ...
  *   $obj->saveTransaction();
  *
  * Assuming ##$obj##, ##$other## and ##$another## live on the same database,
  * this code will work correctly by establishing savepoints.
  *
  * Selects whose data are used later in the transaction should be included in
  * @{method:beginReadLocking} or @{method:beginWriteLocking} block.
  *
  * @task   conn    Managing Connections
  * @task   config  Configuring Lisk
  * @task   load    Loading Objects
  * @task   info    Examining Objects
  * @task   save    Writing Objects
  * @task   hook    Hooks and Callbacks
  * @task   util    Utilities
  * @task   xaction Managing Transactions
  * @task   isolate Isolation for Unit Testing
  */
-abstract class LiskDAO {
+abstract class LiskDAO extends Phobject {
 
   const CONFIG_IDS                  = 'id-mechanism';
   const CONFIG_TIMESTAMPS           = 'timestamps';
   const CONFIG_AUX_PHID             = 'auxiliary-phid';
   const CONFIG_SERIALIZATION        = 'col-serialization';
   const CONFIG_BINARY               = 'binary';
   const CONFIG_COLUMN_SCHEMA        = 'col-schema';
   const CONFIG_KEY_SCHEMA           = 'key-schema';
   const CONFIG_NO_TABLE             = 'no-table';
   const CONFIG_NO_MUTATE            = 'no-mutate';
 
   const SERIALIZATION_NONE          = 'id';
   const SERIALIZATION_JSON          = 'json';
   const SERIALIZATION_PHP           = 'php';
 
   const IDS_AUTOINCREMENT           = 'ids-auto';
   const IDS_COUNTER                 = 'ids-counter';
   const IDS_MANUAL                  = 'ids-manual';
 
   const COUNTER_TABLE_NAME          = 'lisk_counter';
 
   private static $processIsolationLevel     = 0;
   private static $transactionIsolationLevel = 0;
 
   private $ephemeral = false;
   private $forcedConnection;
 
   private static $connections       = array();
 
   private $inSet = null;
 
   protected $id;
   protected $phid;
   protected $dateCreated;
   protected $dateModified;
 
   /**
    *  Build an empty object.
    *
    *  @return obj Empty object.
    */
   public function __construct() {
     $id_key = $this->getIDKey();
     if ($id_key) {
       $this->$id_key = null;
     }
   }
 
 
 /* -(  Managing Connections  )----------------------------------------------- */
 
 
   /**
    * Establish a live connection to a database service. This method should
    * return a new connection. Lisk handles connection caching and management;
    * do not perform caching deeper in the stack.
    *
    * @param string Mode, either 'r' (reading) or 'w' (reading and writing).
    * @return AphrontDatabaseConnection New database connection.
    * @task conn
    */
   abstract protected function establishLiveConnection($mode);
 
 
   /**
    * Return a namespace for this object's connections in the connection cache.
    * Generally, the database name is appropriate. Two connections are considered
    * equivalent if they have the same connection namespace and mode.
    *
    * @return string Connection namespace for cache
    * @task conn
    */
   abstract protected function getConnectionNamespace();
 
 
   /**
    * Get an existing, cached connection for this object.
    *
    * @param mode Connection mode.
    * @return AprontDatabaseConnection|null  Connection, if it exists in cache.
    * @task conn
    */
   protected function getEstablishedConnection($mode) {
     $key = $this->getConnectionNamespace().':'.$mode;
     if (isset(self::$connections[$key])) {
       return self::$connections[$key];
     }
     return null;
   }
 
 
   /**
    * Store a connection in the connection cache.
    *
    * @param mode Connection mode.
    * @param AphrontDatabaseConnection Connection to cache.
    * @return this
    * @task conn
    */
   protected function setEstablishedConnection(
     $mode,
     AphrontDatabaseConnection $connection,
     $force_unique = false) {
 
     $key = $this->getConnectionNamespace().':'.$mode;
 
     if ($force_unique) {
       $key .= ':unique';
       while (isset(self::$connections[$key])) {
         $key .= '!';
       }
     }
 
     self::$connections[$key] = $connection;
     return $this;
   }
 
 
   /**
    * Force an object to use a specific connection.
    *
    * This overrides all connection management and forces the object to use
    * a specific connection when interacting with the database.
    *
    * @param AphrontDatabaseConnection Connection to force this object to use.
    * @task conn
    */
   public function setForcedConnection(AphrontDatabaseConnection $connection) {
     $this->forcedConnection = $connection;
     return $this;
   }
 
 
 /* -(  Configuring Lisk  )--------------------------------------------------- */
 
 
   /**
    * Change Lisk behaviors, like ID configuration and timestamps. If you want
    * to change these behaviors, you should override this method in your child
    * class and change the options you're interested in. For example:
    *
    *   protected function getConfiguration() {
    *     return array(
    *       Lisk_DataAccessObject::CONFIG_EXAMPLE => true,
    *     ) + parent::getConfiguration();
    *   }
    *
    * The available options are:
    *
    * CONFIG_IDS
    * Lisk objects need to have a unique identifying ID. The three mechanisms
    * available for generating this ID are IDS_AUTOINCREMENT (default, assumes
    * the ID column is an autoincrement primary key), IDS_MANUAL (you are taking
    * full responsibility for ID management), or IDS_COUNTER (see below).
    *
    * InnoDB does not persist the value of `auto_increment` across restarts,
    * and instead initializes it to `MAX(id) + 1` during startup. This means it
    * may reissue the same autoincrement ID more than once, if the row is deleted
    * and then the database is restarted. To avoid this, you can set an object to
    * use a counter table with IDS_COUNTER. This will generally behave like
    * IDS_AUTOINCREMENT, except that the counter value will persist across
    * restarts and inserts will be slightly slower. If a database stores any
    * DAOs which use this mechanism, you must create a table there with this
    * schema:
    *
    *   CREATE TABLE lisk_counter (
    *     counterName VARCHAR(64) COLLATE utf8_bin PRIMARY KEY,
    *     counterValue BIGINT UNSIGNED NOT NULL
    *   ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
    *
    * CONFIG_TIMESTAMPS
    * Lisk can automatically handle keeping track of a `dateCreated' and
    * `dateModified' column, which it will update when it creates or modifies
    * an object. If you don't want to do this, you may disable this option.
    * By default, this option is ON.
    *
    * CONFIG_AUX_PHID
    * This option can be enabled by being set to some truthy value. The meaning
    * of this value is defined by your PHID generation mechanism. If this option
    * is enabled, a `phid' property will be populated with a unique PHID when an
    * object is created (or if it is saved and does not currently have one). You
    * need to override generatePHID() and hook it into your PHID generation
    * mechanism for this to work. By default, this option is OFF.
    *
    * CONFIG_SERIALIZATION
    * You can optionally provide a column serialization map that will be applied
    * to values when they are written to the database. For example:
    *
    *   self::CONFIG_SERIALIZATION => array(
    *     'complex' => self::SERIALIZATION_JSON,
    *   )
    *
    * This will cause Lisk to JSON-serialize the 'complex' field before it is
    * written, and unserialize it when it is read.
    *
    * CONFIG_BINARY
    * You can optionally provide a map of columns to a flag indicating that
    * they store binary data. These columns will not raise an error when
    * handling binary writes.
    *
    * CONFIG_COLUMN_SCHEMA
    * Provide a map of columns to schema column types.
    *
    * CONFIG_KEY_SCHEMA
    * Provide a map of key names to key specifications.
    *
    * CONFIG_NO_TABLE
    * Allows you to specify that this object does not actually have a table in
    * the database.
    *
    * CONFIG_NO_MUTATE
    * Provide a map of columns which should not be included in UPDATE statements.
    * If you have some columns which are always written to explicitly and should
    * never be overwritten by a save(), you can specify them here. This is an
    * advanced, specialized feature and there are usually better approaches for
    * most locking/contention problems.
    *
    * @return dictionary  Map of configuration options to values.
    *
    * @task   config
    */
   protected function getConfiguration() {
     return array(
       self::CONFIG_IDS                      => self::IDS_AUTOINCREMENT,
       self::CONFIG_TIMESTAMPS               => true,
     );
   }
 
 
   /**
    *  Determine the setting of a configuration option for this class of objects.
    *
    *  @param  const       Option name, one of the CONFIG_* constants.
    *  @return mixed       Option value, if configured (null if unavailable).
    *
    *  @task   config
    */
   public function getConfigOption($option_name) {
     static $options = null;
 
     if (!isset($options)) {
       $options = $this->getConfiguration();
     }
 
     return idx($options, $option_name);
   }
 
 
 /* -(  Loading Objects  )---------------------------------------------------- */
 
 
   /**
    * Load an object by ID. You need to invoke this as an instance method, not
    * a class method, because PHP doesn't have late static binding (until
    * PHP 5.3.0). For example:
    *
    *   $dog = id(new Dog())->load($dog_id);
    *
    * @param  int       Numeric ID identifying the object to load.
    * @return obj|null  Identified object, or null if it does not exist.
    *
    * @task   load
    */
   public function load($id) {
     if (is_object($id)) {
       $id = (string)$id;
     }
 
     if (!$id || (!is_int($id) && !ctype_digit($id))) {
       return null;
     }
 
     return $this->loadOneWhere(
       '%C = %d',
       $this->getIDKeyForUse(),
       $id);
   }
 
 
   /**
    * Loads all of the objects, unconditionally.
    *
    * @return dict    Dictionary of all persisted objects of this type, keyed
    *                 on object ID.
    *
    * @task   load
    */
   public function loadAll() {
     return $this->loadAllWhere('1 = 1');
   }
 
 
   /**
    * Load all objects which match a WHERE clause. You provide everything after
    * the 'WHERE'; Lisk handles everything up to it. For example:
    *
    *   $old_dogs = id(new Dog())->loadAllWhere('age > %d', 7);
    *
    * The pattern and arguments are as per queryfx().
    *
    * @param  string  queryfx()-style SQL WHERE clause.
    * @param  ...     Zero or more conversions.
    * @return dict    Dictionary of matching objects, keyed on ID.
    *
    * @task   load
    */
   public function loadAllWhere($pattern /* , $arg, $arg, $arg ... */) {
     $args = func_get_args();
     $data = call_user_func_array(
       array($this, 'loadRawDataWhere'),
       $args);
     return $this->loadAllFromArray($data);
   }
 
 
   /**
    * Load a single object identified by a 'WHERE' clause. You provide
    * everything after the 'WHERE', and Lisk builds the first half of the
    * query. See loadAllWhere(). This method is similar, but returns a single
    * result instead of a list.
    *
    * @param  string    queryfx()-style SQL WHERE clause.
    * @param  ...       Zero or more conversions.
    * @return obj|null  Matching object, or null if no object matches.
    *
    * @task   load
    */
   public function loadOneWhere($pattern /* , $arg, $arg, $arg ... */) {
     $args = func_get_args();
     $data = call_user_func_array(
       array($this, 'loadRawDataWhere'),
       $args);
 
     if (count($data) > 1) {
       throw new AphrontCountQueryException(
         pht(
           'More than one result from %s!',
           __FUNCTION__.'()'));
     }
 
     $data = reset($data);
     if (!$data) {
       return null;
     }
 
     return $this->loadFromArray($data);
   }
 
 
   protected function loadRawDataWhere($pattern /* , $args... */) {
     $connection = $this->establishConnection('r');
 
     $lock_clause = '';
     if ($connection->isReadLocking()) {
       $lock_clause = 'FOR UPDATE';
     } else if ($connection->isWriteLocking()) {
       $lock_clause = 'LOCK IN SHARE MODE';
     }
 
     $args = func_get_args();
     $args = array_slice($args, 1);
 
     $pattern = 'SELECT * FROM %T WHERE '.$pattern.' %Q';
     array_unshift($args, $this->getTableName());
     array_push($args, $lock_clause);
     array_unshift($args, $pattern);
 
     return call_user_func_array(
       array($connection, 'queryData'),
       $args);
   }
 
 
   /**
    * Reload an object from the database, discarding any changes to persistent
    * properties. This is primarily useful after entering a transaction but
    * before applying changes to an object.
    *
    * @return this
    *
    * @task   load
    */
   public function reload() {
     if (!$this->getID()) {
       throw new Exception(
         pht("Unable to reload object that hasn't been loaded!"));
     }
 
     $result = $this->loadOneWhere(
       '%C = %d',
       $this->getIDKeyForUse(),
       $this->getID());
 
     if (!$result) {
       throw new AphrontObjectMissingQueryException();
     }
 
     return $this;
   }
 
 
   /**
    * Initialize this object's properties from a dictionary. Generally, you
    * load single objects with loadOneWhere(), but sometimes it may be more
    * convenient to pull data from elsewhere directly (e.g., a complicated
    * join via @{method:queryData}) and then load from an array representation.
    *
    * @param  dict  Dictionary of properties, which should be equivalent to
    *               selecting a row from the table or calling
    *               @{method:getProperties}.
    * @return this
    *
    * @task   load
    */
   public function loadFromArray(array $row) {
     static $valid_properties = array();
 
     $map = array();
     foreach ($row as $k => $v) {
       // We permit (but ignore) extra properties in the array because a
       // common approach to building the array is to issue a raw SELECT query
       // which may include extra explicit columns or joins.
 
       // This pathway is very hot on some pages, so we're inlining a cache
       // and doing some microoptimization to avoid a strtolower() call for each
       // assignment. The common path (assigning a valid property which we've
       // already seen) always incurs only one empty(). The second most common
       // path (assigning an invalid property which we've already seen) costs
       // an empty() plus an isset().
 
       if (empty($valid_properties[$k])) {
         if (isset($valid_properties[$k])) {
           // The value is set but empty, which means it's false, so we've
           // already determined it's not valid. We don't need to check again.
           continue;
         }
         $valid_properties[$k] = $this->hasProperty($k);
         if (!$valid_properties[$k]) {
           continue;
         }
       }
 
       $map[$k] = $v;
     }
 
     $this->willReadData($map);
 
     foreach ($map as $prop => $value) {
       $this->$prop = $value;
     }
 
     $this->didReadData();
 
     return $this;
   }
 
 
   /**
    * Initialize a list of objects from a list of dictionaries. Usually you
    * load lists of objects with @{method:loadAllWhere}, but sometimes that
    * isn't flexible enough. One case is if you need to do joins to select the
    * right objects:
    *
    *   function loadAllWithOwner($owner) {
    *     $data = $this->queryData(
    *       'SELECT d.*
    *         FROM owner o
    *           JOIN owner_has_dog od ON o.id = od.ownerID
    *           JOIN dog d ON od.dogID = d.id
    *         WHERE o.id = %d',
    *       $owner);
    *     return $this->loadAllFromArray($data);
    *   }
    *
    * This is a lot messier than @{method:loadAllWhere}, but more flexible.
    *
    * @param  list  List of property dictionaries.
    * @return dict  List of constructed objects, keyed on ID.
    *
    * @task   load
    */
   public function loadAllFromArray(array $rows) {
     $result = array();
 
     $id_key = $this->getIDKey();
 
     foreach ($rows as $row) {
       $obj = clone $this;
       if ($id_key && isset($row[$id_key])) {
         $result[$row[$id_key]] = $obj->loadFromArray($row);
       } else {
         $result[] = $obj->loadFromArray($row);
       }
       if ($this->inSet) {
         $this->inSet->addToSet($obj);
       }
     }
 
     return $result;
   }
 
   /**
    * This method helps to prevent the 1+N queries problem. It happens when you
    * execute a query for each row in a result set. Like in this code:
    *
    *   COUNTEREXAMPLE, name=Easy to write but expensive to execute
    *   $diffs = id(new DifferentialDiff())->loadAllWhere(
    *     'revisionID = %d',
    *     $revision->getID());
    *   foreach ($diffs as $diff) {
    *     $changesets = id(new DifferentialChangeset())->loadAllWhere(
    *       'diffID = %d',
    *       $diff->getID());
    *     // Do something with $changesets.
    *   }
    *
    * One can solve this problem by reading all the dependent objects at once and
    * assigning them later:
    *
    *   COUNTEREXAMPLE, name=Cheaper to execute but harder to write and maintain
    *   $diffs = id(new DifferentialDiff())->loadAllWhere(
    *     'revisionID = %d',
    *     $revision->getID());
    *   $all_changesets = id(new DifferentialChangeset())->loadAllWhere(
    *     'diffID IN (%Ld)',
    *     mpull($diffs, 'getID'));
    *   $all_changesets = mgroup($all_changesets, 'getDiffID');
    *   foreach ($diffs as $diff) {
    *     $changesets = idx($all_changesets, $diff->getID(), array());
    *     // Do something with $changesets.
    *   }
    *
    * The method @{method:loadRelatives} abstracts this approach which allows
    * writing a code which is simple and efficient at the same time:
    *
    *   name=Easy to write and cheap to execute
    *   $diffs = $revision->loadRelatives(new DifferentialDiff(), 'revisionID');
    *   foreach ($diffs as $diff) {
    *     $changesets = $diff->loadRelatives(
    *       new DifferentialChangeset(),
    *       'diffID');
    *     // Do something with $changesets.
    *   }
    *
    * This will load dependent objects for all diffs in the first call of
    * @{method:loadRelatives} and use this result for all following calls.
    *
    * The method supports working with set of sets, like in this code:
    *
    *   $diffs = $revision->loadRelatives(new DifferentialDiff(), 'revisionID');
    *   foreach ($diffs as $diff) {
    *     $changesets = $diff->loadRelatives(
    *       new DifferentialChangeset(),
    *       'diffID');
    *     foreach ($changesets as $changeset) {
    *       $hunks = $changeset->loadRelatives(
    *         new DifferentialHunk(),
    *         'changesetID');
    *       // Do something with hunks.
    *     }
    *   }
    *
    * This code will execute just three queries - one to load all diffs, one to
    * load all their related changesets and one to load all their related hunks.
    * You can try to write an equivalent code without using this method as
    * a homework.
    *
    * The method also supports retrieving referenced objects, for example authors
    * of all diffs (using shortcut @{method:loadOneRelative}):
    *
    *   foreach ($diffs as $diff) {
    *     $author = $diff->loadOneRelative(
    *       new PhabricatorUser(),
    *       'phid',
    *       'getAuthorPHID');
    *     // Do something with author.
    *   }
    *
    * It is also possible to specify additional conditions for the `WHERE`
    * clause. Similarly to @{method:loadAllWhere}, you can specify everything
    * after `WHERE` (except `LIMIT`). Contrary to @{method:loadAllWhere}, it is
    * allowed to pass only a constant string (`%` doesn't have a special
    * meaning). This is intentional to avoid mistakes with using data from one
    * row in retrieving other rows. Example of a correct usage:
    *
    *   $status = $author->loadOneRelative(
    *     new PhabricatorCalendarEvent(),
    *     'userPHID',
    *     'getPHID',
    *     '(UNIX_TIMESTAMP() BETWEEN dateFrom AND dateTo)');
    *
    * @param  LiskDAO  Type of objects to load.
    * @param  string   Name of the column in target table.
    * @param  string   Method name in this table.
    * @param  string   Additional constraints on returned rows. It supports no
    *                  placeholders and requires putting the WHERE part into
    *                  parentheses. It's not possible to use LIMIT.
    * @return list     Objects of type $object.
    *
    * @task   load
    */
   public function loadRelatives(
     LiskDAO $object,
     $foreign_column,
     $key_method = 'getID',
     $where = '') {
 
     if (!$this->inSet) {
       id(new LiskDAOSet())->addToSet($this);
     }
     $relatives = $this->inSet->loadRelatives(
       $object,
       $foreign_column,
       $key_method,
       $where);
     return idx($relatives, $this->$key_method(), array());
   }
 
   /**
    * Load referenced row. See @{method:loadRelatives} for details.
    *
    * @param  LiskDAO  Type of objects to load.
    * @param  string   Name of the column in target table.
    * @param  string   Method name in this table.
    * @param  string   Additional constraints on returned rows. It supports no
    *                  placeholders and requires putting the WHERE part into
    *                  parentheses. It's not possible to use LIMIT.
    * @return LiskDAO  Object of type $object or null if there's no such object.
    *
    * @task   load
    */
   final public function loadOneRelative(
     LiskDAO $object,
     $foreign_column,
     $key_method = 'getID',
     $where = '') {
 
     $relatives = $this->loadRelatives(
       $object,
       $foreign_column,
       $key_method,
       $where);
 
     if (!$relatives) {
       return null;
     }
 
     if (count($relatives) > 1) {
       throw new AphrontCountQueryException(
         pht(
           'More than one result from %s!',
           __FUNCTION__.'()'));
     }
 
     return reset($relatives);
   }
 
   final public function putInSet(LiskDAOSet $set) {
     $this->inSet = $set;
     return $this;
   }
 
   final protected function getInSet() {
     return $this->inSet;
   }
 
 
 /* -(  Examining Objects  )-------------------------------------------------- */
 
 
   /**
    * Set unique ID identifying this object. You normally don't need to call this
    * method unless with `IDS_MANUAL`.
    *
    * @param  mixed   Unique ID.
    * @return this
    * @task   save
    */
   public function setID($id) {
     static $id_key = null;
     if ($id_key === null) {
       $id_key = $this->getIDKeyForUse();
     }
     $this->$id_key = $id;
     return $this;
   }
 
 
   /**
    * Retrieve the unique ID identifying this object. This value will be null if
    * the object hasn't been persisted and you didn't set it manually.
    *
    * @return mixed   Unique ID.
    *
    * @task   info
    */
   public function getID() {
     static $id_key = null;
     if ($id_key === null) {
       $id_key = $this->getIDKeyForUse();
     }
     return $this->$id_key;
   }
 
 
   public function getPHID() {
     return $this->phid;
   }
 
 
   /**
    * Test if a property exists.
    *
    * @param   string    Property name.
    * @return  bool      True if the property exists.
    * @task info
    */
   public function hasProperty($property) {
     return (bool)$this->checkProperty($property);
   }
 
 
   /**
    * Retrieve a list of all object properties. This list only includes
    * properties that are declared as protected, and it is expected that
    * all properties returned by this function should be persisted to the
    * database.
    * Properties that should not be persisted must be declared as private.
    *
    * @return dict  Dictionary of normalized (lowercase) to canonical (original
    *               case) property names.
    *
    * @task   info
    */
   protected function getAllLiskProperties() {
     static $properties = null;
     if (!isset($properties)) {
       $class = new ReflectionClass(get_class($this));
       $properties = array();
       foreach ($class->getProperties(ReflectionProperty::IS_PROTECTED) as $p) {
         $properties[strtolower($p->getName())] = $p->getName();
       }
 
       $id_key = $this->getIDKey();
       if ($id_key != 'id') {
         unset($properties['id']);
       }
 
       if (!$this->getConfigOption(self::CONFIG_TIMESTAMPS)) {
         unset($properties['datecreated']);
         unset($properties['datemodified']);
       }
 
       if ($id_key != 'phid' && !$this->getConfigOption(self::CONFIG_AUX_PHID)) {
         unset($properties['phid']);
       }
     }
     return $properties;
   }
 
 
   /**
    * Check if a property exists on this object.
    *
    * @return string|null   Canonical property name, or null if the property
    *                       does not exist.
    *
    * @task   info
    */
   protected function checkProperty($property) {
     static $properties = null;
     if ($properties === null) {
       $properties = $this->getAllLiskProperties();
     }
 
     $property = strtolower($property);
     if (empty($properties[$property])) {
       return null;
     }
 
     return $properties[$property];
   }
 
 
   /**
    * Get or build the database connection for this object.
    *
    * @param  string 'r' for read, 'w' for read/write.
    * @param  bool True to force a new connection. The connection will not
    *              be retrieved from or saved into the connection cache.
    * @return LiskDatabaseConnection   Lisk connection object.
    *
    * @task   info
    */
   public function establishConnection($mode, $force_new = false) {
     if ($mode != 'r' && $mode != 'w') {
       throw new Exception(
         pht(
           "Unknown mode '%s', should be 'r' or 'w'.",
           $mode));
     }
 
     if ($this->forcedConnection) {
       return $this->forcedConnection;
     }
 
     if (self::shouldIsolateAllLiskEffectsToCurrentProcess()) {
       $mode = 'isolate-'.$mode;
 
       $connection = $this->getEstablishedConnection($mode);
       if (!$connection) {
         $connection = $this->establishIsolatedConnection($mode);
         $this->setEstablishedConnection($mode, $connection);
       }
 
       return $connection;
     }
 
     if (self::shouldIsolateAllLiskEffectsToTransactions()) {
       // If we're doing fixture transaction isolation, force the mode to 'w'
       // so we always get the same connection for reads and writes, and thus
       // can see the writes inside the transaction.
       $mode = 'w';
     }
 
     // TODO: There is currently no protection on 'r' queries against writing.
 
     $connection = null;
     if (!$force_new) {
       if ($mode == 'r') {
         // If we're requesting a read connection but already have a write
         // connection, reuse the write connection so that reads can take place
         // inside transactions.
         $connection = $this->getEstablishedConnection('w');
       }
 
       if (!$connection) {
         $connection = $this->getEstablishedConnection($mode);
       }
     }
 
     if (!$connection) {
       $connection = $this->establishLiveConnection($mode);
       if (self::shouldIsolateAllLiskEffectsToTransactions()) {
         $connection->openTransaction();
       }
       $this->setEstablishedConnection(
         $mode,
         $connection,
         $force_unique = $force_new);
     }
 
     return $connection;
   }
 
 
   /**
    * Convert this object into a property dictionary. This dictionary can be
    * restored into an object by using @{method:loadFromArray} (unless you're
    * using legacy features with CONFIG_CONVERT_CAMELCASE, but in that case you
    * should just go ahead and die in a fire).
    *
    * @return dict  Dictionary of object properties.
    *
    * @task   info
    */
   protected function getAllLiskPropertyValues() {
     $map = array();
     foreach ($this->getAllLiskProperties() as $p) {
       // We may receive a warning here for properties we've implicitly added
       // through configuration; squelch it.
       $map[$p] = @$this->$p;
     }
     return $map;
   }
 
 
 /* -(  Writing Objects  )---------------------------------------------------- */
 
 
   /**
    * Make an object read-only.
    *
    * Making an object ephemeral indicates that you will be changing state in
    * such a way that you would never ever want it to be written back to the
    * storage.
    */
   public function makeEphemeral() {
     $this->ephemeral = true;
     return $this;
   }
 
   private function isEphemeralCheck() {
     if ($this->ephemeral) {
       throw new LiskEphemeralObjectException();
     }
   }
 
   /**
    * Persist this object to the database. In most cases, this is the only
    * method you need to call to do writes. If the object has not yet been
    * inserted this will do an insert; if it has, it will do an update.
    *
    * @return this
    *
    * @task   save
    */
   public function save() {
     if ($this->shouldInsertWhenSaved()) {
       return $this->insert();
     } else {
       return $this->update();
     }
   }
 
 
   /**
    * Save this object, forcing the query to use REPLACE regardless of object
    * state.
    *
    * @return this
    *
    * @task   save
    */
   public function replace() {
     $this->isEphemeralCheck();
     return $this->insertRecordIntoDatabase('REPLACE');
   }
 
 
   /**
    *  Save this object, forcing the query to use INSERT regardless of object
    *  state.
    *
    *  @return this
    *
    *  @task   save
    */
   public function insert() {
     $this->isEphemeralCheck();
     return $this->insertRecordIntoDatabase('INSERT');
   }
 
 
   /**
    *  Save this object, forcing the query to use UPDATE regardless of object
    *  state.
    *
    *  @return this
    *
    *  @task   save
    */
   public function update() {
     $this->isEphemeralCheck();
 
     $this->willSaveObject();
     $data = $this->getAllLiskPropertyValues();
 
     // Remove colums flagged as nonmutable from the update statement.
     $no_mutate = $this->getConfigOption(self::CONFIG_NO_MUTATE);
     if ($no_mutate) {
       foreach ($no_mutate as $column) {
         unset($data[$column]);
       }
     }
 
     $this->willWriteData($data);
 
     $map = array();
     foreach ($data as $k => $v) {
       $map[$k] = $v;
     }
 
     $conn = $this->establishConnection('w');
     $binary = $this->getBinaryColumns();
 
     foreach ($map as $key => $value) {
       if (!empty($binary[$key])) {
         $map[$key] = qsprintf($conn, '%C = %nB', $key, $value);
       } else {
         $map[$key] = qsprintf($conn, '%C = %ns', $key, $value);
       }
     }
     $map = implode(', ', $map);
 
     $id = $this->getID();
     $conn->query(
       'UPDATE %T SET %Q WHERE %C = '.(is_int($id) ? '%d' : '%s'),
       $this->getTableName(),
       $map,
       $this->getIDKeyForUse(),
       $id);
     // We can't detect a missing object because updating an object without
     // changing any values doesn't affect rows. We could jiggle timestamps
     // to catch this for objects which track them if we wanted.
 
     $this->didWriteData();
 
     return $this;
   }
 
 
   /**
    * Delete this object, permanently.
    *
    * @return this
    *
    * @task   save
    */
   public function delete() {
     $this->isEphemeralCheck();
     $this->willDelete();
 
     $conn = $this->establishConnection('w');
     $conn->query(
       'DELETE FROM %T WHERE %C = %d',
       $this->getTableName(),
       $this->getIDKeyForUse(),
       $this->getID());
 
     $this->didDelete();
 
     return $this;
   }
 
   /**
    * Internal implementation of INSERT and REPLACE.
    *
    * @param  const   Either "INSERT" or "REPLACE", to force the desired mode.
    *
    * @task   save
    */
   protected function insertRecordIntoDatabase($mode) {
     $this->willSaveObject();
     $data = $this->getAllLiskPropertyValues();
 
     $conn = $this->establishConnection('w');
 
     $id_mechanism = $this->getConfigOption(self::CONFIG_IDS);
     switch ($id_mechanism) {
       case self::IDS_AUTOINCREMENT:
         // If we are using autoincrement IDs, let MySQL assign the value for the
         // ID column, if it is empty. If the caller has explicitly provided a
         // value, use it.
         $id_key = $this->getIDKeyForUse();
         if (empty($data[$id_key])) {
           unset($data[$id_key]);
         }
         break;
       case self::IDS_COUNTER:
         // If we are using counter IDs, assign a new ID if we don't already have
         // one.
         $id_key = $this->getIDKeyForUse();
         if (empty($data[$id_key])) {
           $counter_name = $this->getTableName();
           $id = self::loadNextCounterValue($conn, $counter_name);
           $this->setID($id);
           $data[$id_key] = $id;
         }
         break;
       case self::IDS_MANUAL:
         break;
       default:
         throw new Exception(pht('Unknown %s mechanism!', 'CONFIG_IDs'));
     }
 
     $this->willWriteData($data);
 
     $columns = array_keys($data);
     $binary = $this->getBinaryColumns();
 
     foreach ($data as $key => $value) {
       try {
         if (!empty($binary[$key])) {
           $data[$key] = qsprintf($conn, '%nB', $value);
         } else {
           $data[$key] = qsprintf($conn, '%ns', $value);
         }
       } catch (AphrontParameterQueryException $parameter_exception) {
         throw new PhutilProxyException(
           pht(
             "Unable to insert or update object of class %s, field '%s' ".
             "has a non-scalar value.",
             get_class($this),
             $key),
           $parameter_exception);
       }
     }
     $data = implode(', ', $data);
 
     $conn->query(
       '%Q INTO %T (%LC) VALUES (%Q)',
       $mode,
       $this->getTableName(),
       $columns,
       $data);
 
     // Only use the insert id if this table is using auto-increment ids
     if ($id_mechanism === self::IDS_AUTOINCREMENT) {
       $this->setID($conn->getInsertID());
     }
 
     $this->didWriteData();
 
     return $this;
   }
 
 
   /**
    * Method used to determine whether to insert or update when saving.
    *
    * @return bool true if the record should be inserted
    */
   protected function shouldInsertWhenSaved() {
     $key_type = $this->getConfigOption(self::CONFIG_IDS);
 
     if ($key_type == self::IDS_MANUAL) {
       throw new Exception(
         pht(
           'You are using manual IDs. You must override the %s method '.
           'to properly detect when to insert a new record.',
           __FUNCTION__.'()'));
     } else {
       return !$this->getID();
     }
   }
 
 
 /* -(  Hooks and Callbacks  )------------------------------------------------ */
 
 
   /**
    * Retrieve the database table name. By default, this is the class name.
    *
    * @return string  Table name for object storage.
    *
    * @task   hook
    */
   public function getTableName() {
     return get_class($this);
   }
 
 
   /**
    * Retrieve the primary key column, "id" by default. If you can not
    * reasonably name your ID column "id", override this method.
    *
    * @return string  Name of the ID column.
    *
    * @task   hook
    */
   public function getIDKey() {
     return 'id';
   }
 
 
   protected function getIDKeyForUse() {
     $id_key = $this->getIDKey();
     if (!$id_key) {
       throw new Exception(
         pht(
           'This DAO does not have a single-part primary key. The method you '.
           'called requires a single-part primary key.'));
     }
     return $id_key;
   }
 
 
   /**
    * Generate a new PHID, used by CONFIG_AUX_PHID.
    *
    * @return phid    Unique, newly allocated PHID.
    *
    * @task   hook
    */
   public function generatePHID() {
     throw new Exception(
       pht(
         'To use %s, you need to overload %s to perform PHID generation.',
         'CONFIG_AUX_PHID',
         'generatePHID()'));
   }
 
 
   /**
    * Hook to apply serialization or validation to data before it is written to
    * the database. See also @{method:willReadData}.
    *
    * @task hook
    */
   protected function willWriteData(array &$data) {
     $this->applyLiskDataSerialization($data, false);
   }
 
 
   /**
    * Hook to perform actions after data has been written to the database.
    *
    * @task hook
    */
   protected function didWriteData() {}
 
 
   /**
    * Hook to make internal object state changes prior to INSERT, REPLACE or
    * UPDATE.
    *
    * @task hook
    */
   protected function willSaveObject() {
     $use_timestamps = $this->getConfigOption(self::CONFIG_TIMESTAMPS);
 
     if ($use_timestamps) {
       if (!$this->getDateCreated()) {
         $this->setDateCreated(time());
       }
       $this->setDateModified(time());
     }
 
     if ($this->getConfigOption(self::CONFIG_AUX_PHID) && !$this->getPHID()) {
       $this->setPHID($this->generatePHID());
     }
   }
 
 
   /**
    * Hook to apply serialization or validation to data as it is read from the
    * database. See also @{method:willWriteData}.
    *
    * @task hook
    */
   protected function willReadData(array &$data) {
     $this->applyLiskDataSerialization($data, $deserialize = true);
   }
 
   /**
    * Hook to perform an action on data after it is read from the database.
    *
    * @task hook
    */
   protected function didReadData() {}
 
   /**
    * Hook to perform an action before the deletion of an object.
    *
    * @task hook
    */
   protected function willDelete() {}
 
   /**
    * Hook to perform an action after the deletion of an object.
    *
    * @task hook
    */
   protected function didDelete() {}
 
   /**
    * Reads the value from a field. Override this method for custom behavior
    * of @{method:getField} instead of overriding getField directly.
    *
    * @param  string  Canonical field name
    * @return mixed   Value of the field
    *
    * @task hook
    */
   protected function readField($field) {
     if (isset($this->$field)) {
       return $this->$field;
     }
     return null;
   }
 
   /**
    * Writes a value to a field. Override this method for custom behavior of
    * setField($value) instead of overriding setField directly.
    *
    * @param  string  Canonical field name
    * @param  mixed   Value to write
    *
    * @task hook
    */
   protected function writeField($field, $value) {
     $this->$field = $value;
   }
 
 
 /* -(  Manging Transactions  )----------------------------------------------- */
 
 
   /**
    * Increase transaction stack depth.
    *
    * @return this
    */
   public function openTransaction() {
     $this->establishConnection('w')->openTransaction();
     return $this;
   }
 
 
   /**
    * Decrease transaction stack depth, saving work.
    *
    * @return this
    */
   public function saveTransaction() {
     $this->establishConnection('w')->saveTransaction();
     return $this;
   }
 
 
   /**
    * Decrease transaction stack depth, discarding work.
    *
    * @return this
    */
   public function killTransaction() {
     $this->establishConnection('w')->killTransaction();
     return $this;
   }
 
 
   /**
    * Begins read-locking selected rows with SELECT ... FOR UPDATE, so that
    * other connections can not read them (this is an enormous oversimplification
    * of FOR UPDATE semantics; consult the MySQL documentation for details). To
    * end read locking, call @{method:endReadLocking}. For example:
    *
    *   $beach->openTransaction();
    *     $beach->beginReadLocking();
    *
    *       $beach->reload();
    *       $beach->setGrainsOfSand($beach->getGrainsOfSand() + 1);
    *       $beach->save();
    *
    *     $beach->endReadLocking();
    *   $beach->saveTransaction();
    *
    * @return this
    * @task xaction
    */
   public function beginReadLocking() {
     $this->establishConnection('w')->beginReadLocking();
     return $this;
   }
 
 
   /**
    * Ends read-locking that began at an earlier @{method:beginReadLocking} call.
    *
    * @return this
    * @task xaction
    */
   public function endReadLocking() {
     $this->establishConnection('w')->endReadLocking();
     return $this;
   }
 
   /**
    * Begins write-locking selected rows with SELECT ... LOCK IN SHARE MODE, so
    * that other connections can not update or delete them (this is an
    * oversimplification of LOCK IN SHARE MODE semantics; consult the
    * MySQL documentation for details). To end write locking, call
    * @{method:endWriteLocking}.
    *
    * @return this
    * @task xaction
    */
   public function beginWriteLocking() {
     $this->establishConnection('w')->beginWriteLocking();
     return $this;
   }
 
 
   /**
    * Ends write-locking that began at an earlier @{method:beginWriteLocking}
    * call.
    *
    * @return this
    * @task xaction
    */
   public function endWriteLocking() {
     $this->establishConnection('w')->endWriteLocking();
     return $this;
   }
 
 
 /* -(  Isolation  )---------------------------------------------------------- */
 
 
   /**
    * @task isolate
    */
   public static function beginIsolateAllLiskEffectsToCurrentProcess() {
     self::$processIsolationLevel++;
   }
 
   /**
    * @task isolate
    */
   public static function endIsolateAllLiskEffectsToCurrentProcess() {
     self::$processIsolationLevel--;
     if (self::$processIsolationLevel < 0) {
       throw new Exception(
         pht('Lisk process isolation level was reduced below 0.'));
     }
   }
 
   /**
    * @task isolate
    */
   public static function shouldIsolateAllLiskEffectsToCurrentProcess() {
     return (bool)self::$processIsolationLevel;
   }
 
   /**
    * @task isolate
    */
   private function establishIsolatedConnection($mode) {
     $config = array();
     return new AphrontIsolatedDatabaseConnection($config);
   }
 
   /**
    * @task isolate
    */
   public static function beginIsolateAllLiskEffectsToTransactions() {
     if (self::$transactionIsolationLevel === 0) {
       self::closeAllConnections();
     }
     self::$transactionIsolationLevel++;
   }
 
   /**
    * @task isolate
    */
   public static function endIsolateAllLiskEffectsToTransactions() {
     self::$transactionIsolationLevel--;
     if (self::$transactionIsolationLevel < 0) {
       throw new Exception(
         pht('Lisk transaction isolation level was reduced below 0.'));
     } else if (self::$transactionIsolationLevel == 0) {
       foreach (self::$connections as $key => $conn) {
         if ($conn) {
           $conn->killTransaction();
         }
       }
       self::closeAllConnections();
     }
   }
 
   /**
    * @task isolate
    */
   public static function shouldIsolateAllLiskEffectsToTransactions() {
     return (bool)self::$transactionIsolationLevel;
   }
 
   public static function closeAllConnections() {
     self::$connections = array();
   }
 
 /* -(  Utilities  )---------------------------------------------------------- */
 
 
   /**
    * Applies configured serialization to a dictionary of values.
    *
    * @task util
    */
   protected function applyLiskDataSerialization(array &$data, $deserialize) {
     $serialization = $this->getConfigOption(self::CONFIG_SERIALIZATION);
     if ($serialization) {
       foreach (array_intersect_key($serialization, $data) as $col => $format) {
         switch ($format) {
           case self::SERIALIZATION_NONE:
             break;
           case self::SERIALIZATION_PHP:
             if ($deserialize) {
               $data[$col] = unserialize($data[$col]);
             } else {
               $data[$col] = serialize($data[$col]);
             }
             break;
           case self::SERIALIZATION_JSON:
             if ($deserialize) {
               $data[$col] = json_decode($data[$col], true);
             } else {
               $data[$col] = json_encode($data[$col]);
             }
             break;
           default:
             throw new Exception(
               pht("Unknown serialization format '%s'.", $format));
         }
       }
     }
   }
 
   /**
    * Black magic. Builds implied get*() and set*() for all properties.
    *
    * @param  string  Method name.
    * @param  list    Argument vector.
    * @return mixed   get*() methods return the property value. set*() methods
    *                 return $this.
    * @task   util
    */
   public function __call($method, $args) {
     // NOTE: PHP has a bug that static variables defined in __call() are shared
     // across all children classes. Call a different method to work around this
     // bug.
     return $this->call($method, $args);
   }
 
   /**
    * @task   util
    */
   final protected function call($method, $args) {
     // NOTE: This method is very performance-sensitive (many thousands of calls
     // per page on some pages), and thus has some silliness in the name of
     // optimizations.
 
     static $dispatch_map = array();
 
     if ($method[0] === 'g') {
       if (isset($dispatch_map[$method])) {
         $property = $dispatch_map[$method];
       } else {
         if (substr($method, 0, 3) !== 'get') {
           throw new Exception(pht("Unable to resolve method '%s'!", $method));
         }
         $property = substr($method, 3);
         if (!($property = $this->checkProperty($property))) {
           throw new Exception(pht('Bad getter call: %s', $method));
         }
         $dispatch_map[$method] = $property;
       }
 
       return $this->readField($property);
     }
 
     if ($method[0] === 's') {
       if (isset($dispatch_map[$method])) {
         $property = $dispatch_map[$method];
       } else {
         if (substr($method, 0, 3) !== 'set') {
           throw new Exception(pht("Unable to resolve method '%s'!", $method));
         }
         $property = substr($method, 3);
         $property = $this->checkProperty($property);
         if (!$property) {
           throw new Exception(pht('Bad setter call: %s', $method));
         }
         $dispatch_map[$method] = $property;
       }
 
       $this->writeField($property, $args[0]);
 
       return $this;
     }
 
     throw new Exception(pht("Unable to resolve method '%s'.", $method));
   }
 
   /**
    * Warns against writing to undeclared property.
    *
    * @task   util
    */
   public function __set($name, $value) {
     phlog(
       pht(
         'Wrote to undeclared property %s.',
         get_class($this).'::$'.$name));
     $this->$name = $value;
   }
 
 
   /**
    * Increments a named counter and returns the next value.
    *
    * @param   AphrontDatabaseConnection   Database where the counter resides.
    * @param   string                      Counter name to create or increment.
    * @return  int                         Next counter value.
    *
    * @task util
    */
   public static function loadNextCounterValue(
     AphrontDatabaseConnection $conn_w,
     $counter_name) {
 
     // NOTE: If an insert does not touch an autoincrement row or call
     // LAST_INSERT_ID(), MySQL normally does not change the value of
     // LAST_INSERT_ID(). This can cause a counter's value to leak to a
     // new counter if the second counter is created after the first one is
     // updated. To avoid this, we insert LAST_INSERT_ID(1), to ensure the
     // LAST_INSERT_ID() is always updated and always set correctly after the
     // query completes.
 
     queryfx(
       $conn_w,
       'INSERT INTO %T (counterName, counterValue) VALUES
           (%s, LAST_INSERT_ID(1))
         ON DUPLICATE KEY UPDATE
           counterValue = LAST_INSERT_ID(counterValue + 1)',
       self::COUNTER_TABLE_NAME,
       $counter_name);
 
     return $conn_w->getInsertID();
   }
 
 
   /**
    * Returns the current value of a named counter.
    *
    * @param AphrontDatabaseConnection Database where the counter resides.
    * @param string Counter name to read.
    * @return int|null Current value, or `null` if the counter does not exist.
    *
    * @task util
    */
   public static function loadCurrentCounterValue(
     AphrontDatabaseConnection $conn_r,
     $counter_name) {
 
     $row = queryfx_one(
       $conn_r,
       'SELECT counterValue FROM %T WHERE counterName = %s',
       self::COUNTER_TABLE_NAME,
       $counter_name);
     if (!$row) {
       return null;
     }
 
     return (int)$row['counterValue'];
   }
 
 
   /**
    * Overwrite a named counter, forcing it to a specific value.
    *
    * If the counter does not exist, it is created.
    *
    * @param AphrontDatabaseConnection Database where the counter resides.
    * @param string Counter name to create or overwrite.
    * @return void
    *
    * @task util
    */
   public static function overwriteCounterValue(
     AphrontDatabaseConnection $conn_w,
     $counter_name,
     $counter_value) {
 
     queryfx(
       $conn_w,
       'INSERT INTO %T (counterName, counterValue) VALUES (%s, %d)
         ON DUPLICATE KEY UPDATE counterValue = VALUES(counterValue)',
       self::COUNTER_TABLE_NAME,
       $counter_name,
       $counter_value);
   }
 
   private function getBinaryColumns() {
     return $this->getConfigOption(self::CONFIG_BINARY);
   }
 
 
   public function getSchemaColumns() {
     $custom_map = $this->getConfigOption(self::CONFIG_COLUMN_SCHEMA);
     if (!$custom_map) {
       $custom_map = array();
     }
 
     $serialization = $this->getConfigOption(self::CONFIG_SERIALIZATION);
     if (!$serialization) {
       $serialization = array();
     }
 
     $serialization_map = array(
       self::SERIALIZATION_JSON => 'text',
       self::SERIALIZATION_PHP => 'bytes',
     );
 
     $binary_map = $this->getBinaryColumns();
 
     $id_mechanism = $this->getConfigOption(self::CONFIG_IDS);
     if ($id_mechanism == self::IDS_AUTOINCREMENT) {
       $id_type = 'auto';
     } else {
       $id_type = 'id';
     }
 
     $builtin = array(
       'id' => $id_type,
       'phid' => 'phid',
       'viewPolicy' => 'policy',
       'editPolicy' => 'policy',
       'epoch' => 'epoch',
       'dateCreated' => 'epoch',
       'dateModified' => 'epoch',
     );
 
     $map = array();
     foreach ($this->getAllLiskProperties() as $property) {
       // First, use types specified explicitly in the table configuration.
       if (array_key_exists($property, $custom_map)) {
         $map[$property] = $custom_map[$property];
         continue;
       }
 
       // If we don't have an explicit type, try a builtin type for the
       // column.
       $type = idx($builtin, $property);
       if ($type) {
         $map[$property] = $type;
         continue;
       }
 
       // If the column has serialization, we can infer the column type.
       if (isset($serialization[$property])) {
         $type = idx($serialization_map, $serialization[$property]);
         if ($type) {
           $map[$property] = $type;
           continue;
         }
       }
 
       if (isset($binary_map[$property])) {
         $map[$property] = 'bytes';
         continue;
       }
 
       if ($property === 'spacePHID') {
         $map[$property] = 'phid?';
         continue;
       }
 
       // If the column is named `somethingPHID`, infer it is a PHID.
       if (preg_match('/[a-z]PHID$/', $property)) {
         $map[$property] = 'phid';
         continue;
       }
 
       // If the column is named `somethingID`, infer it is an ID.
       if (preg_match('/[a-z]ID$/', $property)) {
         $map[$property] = 'id';
         continue;
       }
 
       // We don't know the type of this column.
       $map[$property] = PhabricatorConfigSchemaSpec::DATATYPE_UNKNOWN;
     }
 
     return $map;
   }
 
   public function getSchemaKeys() {
     $custom_map = $this->getConfigOption(self::CONFIG_KEY_SCHEMA);
     if (!$custom_map) {
       $custom_map = array();
     }
 
     $default_map = array();
     foreach ($this->getAllLiskProperties() as $property) {
       switch ($property) {
         case 'id':
           $default_map['PRIMARY'] = array(
             'columns' => array('id'),
             'unique' => true,
           );
           break;
         case 'phid':
           $default_map['key_phid'] = array(
             'columns' => array('phid'),
             'unique' => true,
           );
           break;
         case 'spacePHID':
           $default_map['key_space'] = array(
             'columns' => array('spacePHID'),
           );
           break;
       }
     }
 
     return $custom_map + $default_map;
   }
 
 }
diff --git a/src/infrastructure/storage/lisk/LiskDAOSet.php b/src/infrastructure/storage/lisk/LiskDAOSet.php
index f81f54f9c..e1bc1e50d 100644
--- a/src/infrastructure/storage/lisk/LiskDAOSet.php
+++ b/src/infrastructure/storage/lisk/LiskDAOSet.php
@@ -1,92 +1,92 @@
 <?php
 
 /**
  * You usually don't need to use this class directly as it is controlled by
  * @{class:LiskDAO}. You can create it if you want to work with objects of same
  * type from different sources as with one set. Let's say you want to get
  * e-mails of all users involved in a revision:
  *
  *   $users = new LiskDAOSet();
  *   $users->addToSet($author);
  *   foreach ($reviewers as $reviewer) {
  *     $users->addToSet($reviewer);
  *   }
  *   foreach ($ccs as $cc) {
  *     $users->addToSet($cc);
  *   }
  *   // Preload e-mails of all involved users and return e-mails of author.
  *   $author_emails = $author->loadRelatives(
  *     new PhabricatorUserEmail(),
  *     'userPHID',
  *     'getPHID');
  */
-final class LiskDAOSet {
+final class LiskDAOSet extends Phobject {
   private $daos = array();
   private $relatives = array();
   private $subsets = array();
 
   public function addToSet(LiskDAO $dao) {
     if ($this->relatives) {
       throw new Exception(
         pht(
           "Don't call %s after loading data!",
           __FUNCTION__.'()'));
     }
     $this->daos[] = $dao;
     $dao->putInSet($this);
     return $this;
   }
 
   /**
    * The main purpose of this method is to break cyclic dependency.
    * It removes all objects from this set and all subsets created by it.
    */
   public function clearSet() {
     $this->daos = array();
     $this->relatives = array();
     foreach ($this->subsets as $set) {
       $set->clearSet();
     }
     $this->subsets = array();
     return $this;
   }
 
 
   /**
    * See @{method:LiskDAO::loadRelatives}.
    */
   public function loadRelatives(
     LiskDAO $object,
     $foreign_column,
     $key_method = 'getID',
     $where = '') {
 
     $relatives = &$this->relatives[
       get_class($object)."-{$foreign_column}-{$key_method}-{$where}"];
 
     if ($relatives === null) {
       $ids = array();
       foreach ($this->daos as $dao) {
         $id = $dao->$key_method();
         if ($id !== null) {
           $ids[$id] = $id;
         }
       }
       if (!$ids) {
         $relatives = array();
       } else {
         $set = new LiskDAOSet();
         $this->subsets[] = $set;
         $relatives = $object->putInSet($set)->loadAllWhere(
           '%C IN (%Ls) %Q',
           $foreign_column,
           $ids,
           ($where != '' ? 'AND '.$where : ''));
         $relatives = mgroup($relatives, 'get'.$foreign_column);
       }
     }
 
     return $relatives;
   }
 
 }
diff --git a/src/infrastructure/storage/lisk/PhabricatorLiskSerializer.php b/src/infrastructure/storage/lisk/PhabricatorLiskSerializer.php
index a8ce144e2..136383900 100644
--- a/src/infrastructure/storage/lisk/PhabricatorLiskSerializer.php
+++ b/src/infrastructure/storage/lisk/PhabricatorLiskSerializer.php
@@ -1,8 +1,8 @@
 <?php
 
-abstract class PhabricatorLiskSerializer {
+abstract class PhabricatorLiskSerializer extends Phobject {
 
   abstract public function willReadValue($value);
   abstract public function willWriteValue($value);
 
 }
diff --git a/src/infrastructure/storage/management/PhabricatorStorageManagementAPI.php b/src/infrastructure/storage/management/PhabricatorStorageManagementAPI.php
index bff0331c0..0d8fc79bb 100644
--- a/src/infrastructure/storage/management/PhabricatorStorageManagementAPI.php
+++ b/src/infrastructure/storage/management/PhabricatorStorageManagementAPI.php
@@ -1,304 +1,304 @@
 <?php
 
-final class PhabricatorStorageManagementAPI {
+final class PhabricatorStorageManagementAPI extends Phobject {
 
   private $host;
   private $user;
   private $port;
   private $password;
   private $namespace;
   private $conns = array();
   private $disableUTF8MB4;
 
   const CHARSET_DEFAULT = 'CHARSET';
   const CHARSET_SORT = 'CHARSET_SORT';
   const CHARSET_FULLTEXT = 'CHARSET_FULLTEXT';
   const COLLATE_TEXT = 'COLLATE_TEXT';
   const COLLATE_SORT = 'COLLATE_SORT';
   const COLLATE_FULLTEXT = 'COLLATE_FULLTEXT';
 
   public function setDisableUTF8MB4($disable_utf8_mb4) {
     $this->disableUTF8MB4 = $disable_utf8_mb4;
     return $this;
   }
 
   public function getDisableUTF8MB4() {
     return $this->disableUTF8MB4;
   }
 
   public function setNamespace($namespace) {
     $this->namespace = $namespace;
     PhabricatorLiskDAO::pushStorageNamespace($namespace);
     return $this;
   }
 
   public function getNamespace() {
     return $this->namespace;
   }
 
   public function setUser($user) {
     $this->user = $user;
     return $this;
   }
 
   public function getUser() {
     return $this->user;
   }
 
   public function setPassword($password) {
     $this->password = $password;
     return $this;
   }
 
   public function getPassword() {
     return $this->password;
   }
 
   public function setHost($host) {
     $this->host = $host;
     return $this;
   }
 
   public function getHost() {
     return $this->host;
   }
 
   public function setPort($port) {
     $this->port = $port;
     return $this;
   }
 
   public function getPort() {
     return $this->port;
   }
 
   public function getDatabaseName($fragment) {
     return $this->namespace.'_'.$fragment;
   }
 
   public function getDatabaseList(array $patches, $only_living = false) {
     assert_instances_of($patches, 'PhabricatorStoragePatch');
 
     $list = array();
 
     foreach ($patches as $patch) {
       if ($patch->getType() == 'db') {
         if ($only_living && $patch->isDead()) {
           continue;
         }
         $list[] = $this->getDatabaseName($patch->getName());
       }
     }
 
     return $list;
   }
 
   public function getConn($fragment) {
     $database = $this->getDatabaseName($fragment);
     $return = &$this->conns[$this->host][$this->user][$database];
     if (!$return) {
       $return = PhabricatorEnv::newObjectFromConfig(
       'mysql.implementation',
       array(
         array(
           'user'      => $this->user,
           'pass'      => $this->password,
           'host'      => $this->host,
           'port'      => $this->port,
           'database'  => $fragment
             ? $database
             : null,
         ),
       ));
     }
     return $return;
   }
 
   public function getAppliedPatches() {
     try {
       $applied = queryfx_all(
         $this->getConn('meta_data'),
         'SELECT patch FROM patch_status');
       return ipull($applied, 'patch');
     } catch (AphrontQueryException $ex) {
       return null;
     }
   }
 
   public function createDatabase($fragment) {
     $info = $this->getCharsetInfo();
 
     queryfx(
       $this->getConn(null),
       'CREATE DATABASE IF NOT EXISTS %T COLLATE %T',
       $this->getDatabaseName($fragment),
       $info[self::COLLATE_TEXT]);
   }
 
   public function createTable($fragment, $table, array $cols) {
     queryfx(
       $this->getConn($fragment),
       'CREATE TABLE IF NOT EXISTS %T.%T (%Q) '.
       'ENGINE=InnoDB, COLLATE utf8_general_ci',
       $this->getDatabaseName($fragment),
       $table,
       implode(', ', $cols));
   }
 
   public function getLegacyPatches(array $patches) {
     assert_instances_of($patches, 'PhabricatorStoragePatch');
 
     try {
       $row = queryfx_one(
         $this->getConn('meta_data'),
         'SELECT version FROM %T',
         'schema_version');
       $version = $row['version'];
     } catch (AphrontQueryException $ex) {
       return array();
     }
 
     $legacy = array();
     foreach ($patches as $key => $patch) {
       if ($patch->getLegacy() !== false && $patch->getLegacy() <= $version) {
         $legacy[] = $key;
       }
     }
 
     return $legacy;
   }
 
   public function markPatchApplied($patch) {
     queryfx(
       $this->getConn('meta_data'),
       'INSERT INTO %T (patch, applied) VALUES (%s, %d)',
       'patch_status',
       $patch,
       time());
   }
 
   public function applyPatch(PhabricatorStoragePatch $patch) {
     $type = $patch->getType();
     $name = $patch->getName();
     switch ($type) {
       case 'db':
         $this->createDatabase($name);
         break;
       case 'sql':
         $this->applyPatchSQL($name);
         break;
       case 'php':
         $this->applyPatchPHP($name);
         break;
       default:
         throw new Exception(pht("Unable to apply patch of type '%s'.", $type));
     }
   }
 
   public function applyPatchSQL($sql) {
     $sql = Filesystem::readFile($sql);
     $queries = preg_split('/;\s+/', $sql);
     $queries = array_filter($queries);
 
     $conn = $this->getConn(null);
 
     $charset_info = $this->getCharsetInfo();
     foreach ($charset_info as $key => $value) {
       $charset_info[$key] = qsprintf($conn, '%T', $value);
     }
 
     foreach ($queries as $query) {
       $query = str_replace('{$NAMESPACE}', $this->namespace, $query);
 
       foreach ($charset_info as $key => $value) {
         $query = str_replace('{$'.$key.'}', $value, $query);
       }
 
       queryfx(
         $conn,
         '%Q',
         $query);
     }
   }
 
   public function applyPatchPHP($script) {
     $schema_conn = $this->getConn(null);
     require_once $script;
   }
 
   public function isCharacterSetAvailable($character_set) {
     if ($character_set == 'utf8mb4') {
       if ($this->getDisableUTF8MB4()) {
         return false;
       }
     }
 
     $conn = $this->getConn(null);
     return self::isCharacterSetAvailableOnConnection($character_set, $conn);
   }
 
   public static function isCharacterSetAvailableOnConnection(
     $character_set,
     AphrontDatabaseConnection $conn) {
     $result = queryfx_one(
       $conn,
       'SELECT CHARACTER_SET_NAME FROM INFORMATION_SCHEMA.CHARACTER_SETS
         WHERE CHARACTER_SET_NAME = %s',
       $character_set);
 
     return (bool)$result;
   }
 
   public function getCharsetInfo() {
     if ($this->isCharacterSetAvailable('utf8mb4')) {
       // If utf8mb4 is available, we use it with the utf8mb4_unicode_ci
       // collation. This is most correct, and will sort properly.
 
       $charset = 'utf8mb4';
       $charset_sort = 'utf8mb4';
       $charset_full = 'utf8mb4';
       $collate_text = 'utf8mb4_bin';
       $collate_sort = 'utf8mb4_unicode_ci';
       $collate_full = 'utf8mb4_unicode_ci';
     } else {
       // If utf8mb4 is not available, we use binary for most data. This allows
       // us to store 4-byte unicode characters.
       //
       // It's possible that strings will be truncated in the middle of a
       // character on insert. We encourage users to set STRICT_ALL_TABLES
       // to prevent this.
       //
       // For "fulltext" and "sort" columns, we don't use binary.
       //
       // With "fulltext", we can not use binary because MySQL won't let us.
       // We use 3-byte utf8 instead and accept being unable to index 4-byte
       // characters.
       //
       // With "sort", if we use binary we lose case insensitivity (for
       // example, "ALincoln@logcabin.com" and "alincoln@logcabin.com" would no
       // longer be identified as the same email address). This can be very
       // confusing and is far worse overall than not supporting 4-byte unicode
       // characters, so we use 3-byte utf8 and accept limited 4-byte support as
       // a tradeoff to get sensible collation behavior. Many columns where
       // collation is important rarely contain 4-byte characters anyway, so we
       // are not giving up too much.
 
       $charset = 'binary';
       $charset_sort = 'utf8';
       $charset_full = 'utf8';
       $collate_text = 'binary';
       $collate_sort = 'utf8_general_ci';
       $collate_full = 'utf8_general_ci';
     }
 
     return array(
       self::CHARSET_DEFAULT => $charset,
       self::CHARSET_SORT => $charset_sort,
       self::CHARSET_FULLTEXT => $charset_full,
       self::COLLATE_TEXT => $collate_text,
       self::COLLATE_SORT => $collate_sort,
       self::COLLATE_FULLTEXT => $collate_full,
     );
   }
 
 }
diff --git a/src/infrastructure/storage/management/PhabricatorStoragePatch.php b/src/infrastructure/storage/management/PhabricatorStoragePatch.php
index e945f1bf2..6b9ed890b 100644
--- a/src/infrastructure/storage/management/PhabricatorStoragePatch.php
+++ b/src/infrastructure/storage/management/PhabricatorStoragePatch.php
@@ -1,51 +1,51 @@
 <?php
 
-final class PhabricatorStoragePatch {
+final class PhabricatorStoragePatch extends Phobject {
 
   private $key;
   private $fullKey;
   private $name;
   private $type;
   private $after;
   private $legacy;
   private $dead;
 
   public function __construct(array $dict) {
     $this->key      = $dict['key'];
     $this->type     = $dict['type'];
     $this->fullKey  = $dict['fullKey'];
     $this->legacy   = $dict['legacy'];
     $this->name     = $dict['name'];
     $this->after    = $dict['after'];
     $this->dead     = $dict['dead'];
   }
 
   public function getLegacy() {
     return $this->legacy;
   }
 
   public function getAfter() {
     return $this->after;
   }
 
   public function getType() {
     return $this->type;
   }
 
   public function getName() {
     return $this->name;
   }
 
   public function getFullKey() {
     return $this->fullKey;
   }
 
   public function getKey() {
     return $this->key;
   }
 
   public function isDead() {
     return $this->dead;
   }
 
 }
diff --git a/src/infrastructure/storage/patch/PhabricatorSQLPatchList.php b/src/infrastructure/storage/patch/PhabricatorSQLPatchList.php
index fb23cead0..e0be6add1 100644
--- a/src/infrastructure/storage/patch/PhabricatorSQLPatchList.php
+++ b/src/infrastructure/storage/patch/PhabricatorSQLPatchList.php
@@ -1,217 +1,217 @@
 <?php
 
-abstract class PhabricatorSQLPatchList {
+abstract class PhabricatorSQLPatchList extends Phobject {
 
   abstract public function getNamespace();
   abstract public function getPatches();
 
   /**
    * Examine a directory for `.php` and `.sql` files and build patch
    * specifications for them.
    */
   protected function buildPatchesFromDirectory($directory) {
     $patch_list = Filesystem::listDirectory(
       $directory,
       $include_hidden = false);
 
     sort($patch_list);
     $patches = array();
 
     foreach ($patch_list as $patch) {
       $matches = null;
       if (!preg_match('/\.(sql|php)$/', $patch, $matches)) {
         throw new Exception(
           pht(
             'Unknown patch "%s" in "%s", expected ".php" or ".sql" suffix.',
             $patch,
             $directory));
       }
 
       $patches[$patch] = array(
         'type' => $matches[1],
         'name' => rtrim($directory, '/').'/'.$patch,
       );
     }
 
     return $patches;
   }
 
   final public static function buildAllPatches() {
     $patch_lists = id(new PhutilSymbolLoader())
       ->setAncestorClass(__CLASS__)
       ->setConcreteOnly(true)
       ->selectAndLoadSymbols();
 
     $specs = array();
     $seen_namespaces = array();
 
     foreach ($patch_lists as $patch_class) {
       $patch_class = $patch_class['name'];
       $patch_list = newv($patch_class, array());
 
       $namespace = $patch_list->getNamespace();
       if (isset($seen_namespaces[$namespace])) {
         $prior = $seen_namespaces[$namespace];
         throw new Exception(
           pht(
             "%s '%s' has the same namespace, '%s', as another patch list ".
             "class, '%s'. Each patch list MUST have a unique namespace.",
             __CLASS__,
             $patch_class,
             $namespace,
             $prior));
       }
 
       $last_key = null;
       foreach ($patch_list->getPatches() as $key => $patch) {
         if (!is_array($patch)) {
           throw new Exception(
             pht(
               "%s '%s' has a patch '%s' which is not an array.",
               __CLASS__,
               $patch_class,
               $key));
         }
 
         $valid = array(
           'type'    => true,
           'name'    => true,
           'after'   => true,
           'legacy'  => true,
           'dead'    => true,
         );
 
         foreach ($patch as $pkey => $pval) {
           if (empty($valid[$pkey])) {
             throw new Exception(
               pht(
                 "%s '%s' has a patch, '%s', with an unknown property, '%s'.".
                 "Patches must have only valid keys: %s.",
                 __CLASS__,
                 $patch_class,
                 $key,
                 $pkey,
                 implode(', ', array_keys($valid))));
           }
         }
 
         if (is_numeric($key)) {
           throw new Exception(
             pht(
               "%s '%s' has a patch with a numeric key, '%s'. ".
               "Patches must use string keys.",
               __CLASS__,
               $patch_class,
               $key));
         }
 
         if (strpos($key, ':') !== false) {
           throw new Exception(
             pht(
               "%s '%s' has a patch with a colon in the key name, '%s'. ".
               "Patch keys may not contain colons.",
               __CLASS__,
               $patch_class,
               $key));
         }
 
         $full_key = "{$namespace}:{$key}";
 
         if (isset($specs[$full_key])) {
           throw new Exception(
             pht(
               "%s '%s' has a patch '%s' which duplicates an ".
               "existing patch key.",
               __CLASS__,
               $patch_class,
               $key));
         }
 
         $patch['key']     = $key;
         $patch['fullKey'] = $full_key;
         $patch['dead']    = (bool)idx($patch, 'dead', false);
 
         if (isset($patch['legacy'])) {
           if ($namespace != 'phabricator') {
             throw new Exception(
               pht(
                 "Only patches in the '%s' namespace may contain '%s' keys.",
                 'phabricator',
                 'legacy'));
           }
         } else {
           $patch['legacy'] = false;
         }
 
         if (!array_key_exists('after', $patch)) {
           if ($last_key === null) {
             throw new Exception(
               pht(
                 "Patch '%s' is missing key 'after', and is the first patch ".
                 "in the patch list '%s', so its application order can not be ".
                 "determined implicitly. The first patch in a patch list must ".
                 "list the patch or patches it depends on explicitly.",
                 $full_key,
                 $patch_class));
           } else {
             $patch['after'] = array($last_key);
           }
         }
         $last_key = $full_key;
 
         foreach ($patch['after'] as $after_key => $after) {
           if (strpos($after, ':') === false) {
             $patch['after'][$after_key] = $namespace.':'.$after;
           }
         }
 
         $type = idx($patch, 'type');
         if (!$type) {
           throw new Exception(
             pht(
               "Patch '%s' is missing key '%s'. Every patch must have a type.",
               "{$namespace}:{$key}",
               'type'));
         }
 
         switch ($type) {
           case 'db':
           case 'sql':
           case 'php':
             break;
           default:
             throw new Exception(
               pht(
                 "Patch '%s' has unknown patch type '%s'.",
                 "{$namespace}:{$key}",
                 $type));
         }
 
         $specs[$full_key] = $patch;
       }
     }
 
     foreach ($specs as $key => $patch) {
       foreach ($patch['after'] as $after) {
         if (empty($specs[$after])) {
           throw new Exception(
             pht(
               "Patch '%s' references nonexistent dependency, '%s'. ".
               "Patches may only depend on patches which actually exist.",
               $key,
               $after));
         }
       }
     }
 
     $patches = array();
     foreach ($specs as $full_key => $spec) {
       $patches[$full_key] = new PhabricatorStoragePatch($spec);
     }
 
     // TODO: Detect cycles?
 
     return $patches;
   }
 
 }
diff --git a/src/infrastructure/testing/fixture/PhabricatorStorageFixtureScopeGuard.php b/src/infrastructure/testing/fixture/PhabricatorStorageFixtureScopeGuard.php
index d92e8b206..50a7f9d10 100644
--- a/src/infrastructure/testing/fixture/PhabricatorStorageFixtureScopeGuard.php
+++ b/src/infrastructure/testing/fixture/PhabricatorStorageFixtureScopeGuard.php
@@ -1,38 +1,38 @@
 <?php
 
 /**
  * Used by unit tests to build storage fixtures.
  */
-final class PhabricatorStorageFixtureScopeGuard {
+final class PhabricatorStorageFixtureScopeGuard extends Phobject {
 
   private $name;
 
   public function __construct($name) {
     $this->name = $name;
 
     execx(
       'php %s upgrade --force --no-adjust --namespace %s',
       $this->getStorageBinPath(),
       $this->name);
 
     PhabricatorLiskDAO::pushStorageNamespace($name);
 
     // Destructor is not called with fatal error.
     register_shutdown_function(array($this, 'destroy'));
   }
 
   public function destroy() {
     PhabricatorLiskDAO::popStorageNamespace();
 
     execx(
       'php %s destroy --force --namespace %s',
       $this->getStorageBinPath(),
       $this->name);
   }
 
   private function getStorageBinPath() {
     $root = dirname(phutil_get_library_root('phabricator'));
     return $root.'/scripts/sql/manage_storage.php';
   }
 
 }
diff --git a/src/infrastructure/time/PhabricatorTime.php b/src/infrastructure/time/PhabricatorTime.php
index 2adcc95d4..250d8f1e2 100644
--- a/src/infrastructure/time/PhabricatorTime.php
+++ b/src/infrastructure/time/PhabricatorTime.php
@@ -1,81 +1,81 @@
 <?php
 
-final class PhabricatorTime {
+final class PhabricatorTime extends Phobject {
 
   private static $stack = array();
   private static $originalZone;
 
   public static function pushTime($epoch, $timezone) {
     if (empty(self::$stack)) {
       self::$originalZone = date_default_timezone_get();
     }
 
     $ok = date_default_timezone_set($timezone);
     if (!$ok) {
       throw new Exception(pht("Invalid timezone '%s'!", $timezone));
     }
 
     self::$stack[] = array(
       'epoch'       => $epoch,
       'timezone'    => $timezone,
     );
 
     return new PhabricatorTimeGuard(last_key(self::$stack));
   }
 
   public static function popTime($key) {
     if ($key !== last_key(self::$stack)) {
       throw new Exception(
         pht(
           '%s with bad key.',
           __METHOD__));
     }
     array_pop(self::$stack);
 
     if (empty(self::$stack)) {
       date_default_timezone_set(self::$originalZone);
     } else {
       $frame = end(self::$stack);
       date_default_timezone_set($frame['timezone']);
     }
   }
 
   public static function getNow() {
     if (self::$stack) {
       $frame = end(self::$stack);
       return $frame['epoch'];
     }
     return time();
   }
 
   public static function parseLocalTime($time, PhabricatorUser $user) {
     $old_zone = date_default_timezone_get();
 
     date_default_timezone_set($user->getTimezoneIdentifier());
       $timestamp = (int)strtotime($time, self::getNow());
       if ($timestamp <= 0) {
         $timestamp = null;
       }
     date_default_timezone_set($old_zone);
 
     return $timestamp;
   }
 
   public static function getTodayMidnightDateTime($viewer) {
     $timezone = new DateTimeZone($viewer->getTimezoneIdentifier());
     $today = new DateTime('@'.time());
     $today->setTimeZone($timezone);
     $year = $today->format('Y');
     $month = $today->format('m');
     $day = $today->format('d');
     $today = new DateTime("{$year}-{$month}-{$day}", $timezone);
     return $today;
   }
 
   public static function getDateTimeFromEpoch($epoch, PhabricatorUser $viewer) {
     $datetime = new DateTime('@'.$epoch);
     $datetime->setTimeZone($viewer->getTimeZone());
     return $datetime;
   }
 
 }
diff --git a/src/infrastructure/time/PhabricatorTimeGuard.php b/src/infrastructure/time/PhabricatorTimeGuard.php
index 2add3db30..74aeb0e47 100644
--- a/src/infrastructure/time/PhabricatorTimeGuard.php
+++ b/src/infrastructure/time/PhabricatorTimeGuard.php
@@ -1,15 +1,15 @@
 <?php
 
-final class PhabricatorTimeGuard {
+final class PhabricatorTimeGuard extends Phobject {
 
   private $frameKey;
 
   public function __construct($frame_key) {
     $this->frameKey = $frame_key;
   }
 
   public function __destruct() {
     PhabricatorTime::popTime($this->frameKey);
   }
 
 }
diff --git a/src/infrastructure/util/PhabricatorSlug.php b/src/infrastructure/util/PhabricatorSlug.php
index 4e6d761dc..53330391b 100644
--- a/src/infrastructure/util/PhabricatorSlug.php
+++ b/src/infrastructure/util/PhabricatorSlug.php
@@ -1,86 +1,86 @@
 <?php
 
-final class PhabricatorSlug {
+final class PhabricatorSlug extends Phobject {
 
   public static function normalize($slug) {
     $slug = preg_replace('@/+@', '/', $slug);
     $slug = trim($slug, '/');
     $slug = phutil_utf8_strtolower($slug);
     $slug = preg_replace("@[\\x00-\\x19#%&+=\\\\?<> ]+@", '_', $slug);
     $slug = preg_replace('@_+@', '_', $slug);
 
     // Remove leading and trailing underscores from each component, if the
     // component has not been reduced to a single underscore. For example, "a?"
     // converts to "a", but "??" converts to "_".
     $parts = explode('/', $slug);
     foreach ($parts as $key => $part) {
       if ($part != '_') {
         $parts[$key] = trim($part, '_');
       }
     }
     $slug = implode('/', $parts);
 
     // Specifically rewrite these slugs. It's OK to have a slug like "a..b",
     // but not a slug which is only "..".
 
     // NOTE: These are explicitly not pht()'d, because they should be stable
     // across languages.
 
     $replace = array(
       '.'   => 'dot',
       '..'  => 'dotdot',
     );
 
     foreach ($replace as $pattern => $replacement) {
       $pattern = preg_quote($pattern, '@');
       $slug = preg_replace(
         '@(^|/)'.$pattern.'(\z|/)@',
         '\1'.$replacement.'\2', $slug);
     }
 
     return $slug.'/';
   }
 
   public static function getDefaultTitle($slug) {
     $parts = explode('/', trim($slug, '/'));
     $default_title = end($parts);
     $default_title = str_replace('_', ' ', $default_title);
     $default_title = phutil_utf8_ucwords($default_title);
     $default_title = nonempty($default_title, pht('Untitled Document'));
     return $default_title;
   }
 
   public static function getAncestry($slug) {
     $slug = self::normalize($slug);
 
     if ($slug == '/') {
       return array();
     }
 
     $ancestors = array(
       '/',
     );
 
     $slug = explode('/', $slug);
     array_pop($slug);
     array_pop($slug);
 
     $accumulate = '';
     foreach ($slug as $part) {
       $accumulate .= $part.'/';
       $ancestors[] = $accumulate;
     }
 
     return $ancestors;
   }
 
   public static function getDepth($slug) {
     $slug = self::normalize($slug);
     if ($slug == '/') {
       return 0;
     } else {
       return substr_count($slug, '/');
     }
   }
 
 }
diff --git a/src/infrastructure/util/password/__tests__/PhabricatorIteratedMD5PasswordHasherTestCase.php b/src/infrastructure/util/password/__tests__/PhabricatorIteratedMD5PasswordHasherTestCase.php
new file mode 100644
index 000000000..b3644a564
--- /dev/null
+++ b/src/infrastructure/util/password/__tests__/PhabricatorIteratedMD5PasswordHasherTestCase.php
@@ -0,0 +1,15 @@
+<?php
+
+final class PhabricatorIteratedMD5PasswordHasherTestCase
+  extends PhabricatorTestCase {
+
+  public function testHasher() {
+    $hasher = new PhabricatorIteratedMD5PasswordHasher();
+
+    $this->assertEqual(
+      'md5:4824a35493d8b5dceab36f017d68425f',
+      $hasher->getPasswordHashForStorage(
+        new PhutilOpaqueEnvelope('quack'))->openEnvelope());
+  }
+
+}
diff --git a/src/infrastructure/util/password/__tests__/PhabricatorPasswordHasherTestCase.php b/src/infrastructure/util/password/__tests__/PhabricatorPasswordHasherTestCase.php
index 053acea4f..db1bfadf7 100644
--- a/src/infrastructure/util/password/__tests__/PhabricatorPasswordHasherTestCase.php
+++ b/src/infrastructure/util/password/__tests__/PhabricatorPasswordHasherTestCase.php
@@ -1,40 +1,36 @@
 <?php
 
 final class PhabricatorPasswordHasherTestCase extends PhabricatorTestCase {
 
   public function testHasherSyntax() {
     $caught = null;
     try {
       PhabricatorPasswordHasher::getHasherForHash(
         new PhutilOpaqueEnvelope('xxx'));
     } catch (Exception $ex) {
       $caught = $ex;
     }
 
     $this->assertTrue(
       ($caught instanceof Exception),
       pht('Exception on unparseable hash format.'));
 
     $caught = null;
     try {
       PhabricatorPasswordHasher::getHasherForHash(
         new PhutilOpaqueEnvelope('__test__:yyy'));
     } catch (Exception $ex) {
       $caught = $ex;
     }
 
     $this->assertTrue(
       ($caught instanceof PhabricatorPasswordHasherUnavailableException),
       pht('Fictional hasher unavailable.'));
   }
 
-  public function testMD5Hasher() {
-    $hasher = new PhabricatorIteratedMD5PasswordHasher();
-
-    $this->assertEqual(
-      'md5:4824a35493d8b5dceab36f017d68425f',
-      $hasher->getPasswordHashForStorage(
-        new PhutilOpaqueEnvelope('quack'))->openEnvelope());
+  public function testGetAllHashers() {
+    PhabricatorPasswordHasher::getAllHashers();
+    $this->assertTrue(true);
   }
 
 }
diff --git a/src/view/control/PhabricatorObjectSelectorDialog.php b/src/view/control/PhabricatorObjectSelectorDialog.php
index f6e107a99..eaa51e511 100644
--- a/src/view/control/PhabricatorObjectSelectorDialog.php
+++ b/src/view/control/PhabricatorObjectSelectorDialog.php
@@ -1,194 +1,194 @@
 <?php
 
-final class PhabricatorObjectSelectorDialog {
+final class PhabricatorObjectSelectorDialog extends Phobject {
 
   private $user;
   private $filters = array();
   private $handles = array();
   private $cancelURI;
   private $submitURI;
   private $searchURI;
   private $selectedFilter;
   private $excluded;
 
   private $title;
   private $header;
   private $buttonText;
   private $instructions;
 
   public function setUser($user) {
     $this->user = $user;
     return $this;
   }
 
   public function setFilters(array $filters) {
     $this->filters = $filters;
     return $this;
   }
 
   public function setSelectedFilter($selected_filter) {
     $this->selectedFilter = $selected_filter;
     return $this;
   }
 
   public function setExcluded($excluded_phid) {
     $this->excluded = $excluded_phid;
     return $this;
   }
 
   public function setHandles(array $handles) {
     assert_instances_of($handles, 'PhabricatorObjectHandle');
     $this->handles = $handles;
     return $this;
   }
 
   public function setCancelURI($cancel_uri) {
     $this->cancelURI = $cancel_uri;
     return $this;
   }
 
   public function setSubmitURI($submit_uri) {
     $this->submitURI = $submit_uri;
     return $this;
   }
 
   public function setSearchURI($search_uri) {
     $this->searchURI = $search_uri;
     return $this;
   }
 
   public function setTitle($title) {
     $this->title = $title;
     return $this;
   }
 
   public function setHeader($header) {
     $this->header = $header;
     return $this;
   }
 
   public function setButtonText($button_text) {
     $this->buttonText = $button_text;
     return $this;
   }
 
   public function setInstructions($instructions) {
     $this->instructions = $instructions;
     return $this;
   }
 
   public function buildDialog() {
     $user = $this->user;
 
     $filter_id = celerity_generate_unique_node_id();
     $query_id = celerity_generate_unique_node_id();
     $results_id = celerity_generate_unique_node_id();
     $current_id = celerity_generate_unique_node_id();
     $search_id  = celerity_generate_unique_node_id();
     $form_id = celerity_generate_unique_node_id();
 
     require_celerity_resource('phabricator-object-selector-css');
 
     $options = array();
     foreach ($this->filters as $key => $label) {
       $options[] = phutil_tag(
         'option',
         array(
           'value' => $key,
           'selected' => ($key == $this->selectedFilter)
             ? 'selected'
             : null,
         ),
         $label);
     }
 
     $instructions = null;
     if ($this->instructions) {
       $instructions = phutil_tag(
         'p',
         array('class' => 'phabricator-object-selector-instructions'),
         $this->instructions);
     }
 
     $search_box = phabricator_form(
       $user,
       array(
         'method' => 'POST',
         'action' => $this->submitURI,
         'id'     => $search_id,
       ),
       phutil_tag(
         'table',
         array('class' => 'phabricator-object-selector-search'),
         phutil_tag('tr', array(), array(
           phutil_tag(
             'td',
             array('class' => 'phabricator-object-selector-search-filter'),
             phutil_tag('select', array('id' => $filter_id), $options)),
           phutil_tag(
             'td',
             array('class' => 'phabricator-object-selector-search-text'),
             phutil_tag('input', array('id' => $query_id, 'type' => 'text'))),
         ))));
 
     $result_box = phutil_tag(
       'div',
       array(
         'class' => 'phabricator-object-selector-results',
         'id' => $results_id,
       ),
       '');
 
     $attached_box = phutil_tag_div(
       'phabricator-object-selector-current',
       phutil_tag_div(
         'phabricator-object-selector-currently-attached',
         array(
           phutil_tag_div('phabricator-object-selector-header', $this->header),
           phutil_tag('div', array('id' => $current_id)),
           $instructions,
         )));
 
     $dialog = new AphrontDialogView();
     $dialog
       ->setUser($this->user)
       ->setTitle($this->title)
       ->setClass('phabricator-object-selector-dialog')
       ->appendChild($search_box)
       ->appendChild($result_box)
       ->appendChild($attached_box)
       ->setRenderDialogAsDiv()
       ->setFormID($form_id)
       ->addSubmitButton($this->buttonText);
 
     if ($this->cancelURI) {
       $dialog->addCancelButton($this->cancelURI);
     }
 
     $handle_views = array();
     foreach ($this->handles as $handle) {
       $phid = $handle->getPHID();
       $view = new PhabricatorHandleObjectSelectorDataView($handle);
       $handle_views[$phid] = $view->renderData();
     }
     $dialog->addHiddenInput('phids', implode(';', array_keys($this->handles)));
 
 
     Javelin::initBehavior(
       'phabricator-object-selector',
       array(
         'filter'  => $filter_id,
         'query'   => $query_id,
         'search'  => $search_id,
         'results' => $results_id,
         'current' => $current_id,
         'form'    => $form_id,
         'exclude' => $this->excluded,
         'uri'     => $this->searchURI,
         'handles' => $handle_views,
       ));
 
    return $dialog;
   }
 
 }
diff --git a/src/view/form/control/AphrontFormPolicyControl.php b/src/view/form/control/AphrontFormPolicyControl.php
index 2997969c7..7c4fefcac 100644
--- a/src/view/form/control/AphrontFormPolicyControl.php
+++ b/src/view/form/control/AphrontFormPolicyControl.php
@@ -1,234 +1,327 @@
 <?php
 
 final class AphrontFormPolicyControl extends AphrontFormControl {
 
   private $object;
   private $capability;
   private $policies;
+  private $spacePHID;
+  private $templatePHIDType;
+  private $templateObject;
 
   public function setPolicyObject(PhabricatorPolicyInterface $object) {
     $this->object = $object;
     return $this;
   }
 
   public function setPolicies(array $policies) {
     assert_instances_of($policies, 'PhabricatorPolicy');
     $this->policies = $policies;
     return $this;
   }
 
+  public function setSpacePHID($space_phid) {
+    $this->spacePHID = $space_phid;
+    return $this;
+  }
+
+  public function getSpacePHID() {
+    return $this->spacePHID;
+  }
+
+  public function setTemplatePHIDType($type) {
+    $this->templatePHIDType = $type;
+    return $this;
+  }
+
+  public function setTemplateObject($object) {
+    $this->templateObject = $object;
+    return $this;
+  }
+
   public function setCapability($capability) {
     $this->capability = $capability;
 
     $labels = array(
       PhabricatorPolicyCapability::CAN_VIEW => pht('Visible To'),
       PhabricatorPolicyCapability::CAN_EDIT => pht('Editable By'),
       PhabricatorPolicyCapability::CAN_JOIN => pht('Joinable By'),
     );
 
     if (isset($labels[$capability])) {
       $label = $labels[$capability];
     } else {
       $capobj = PhabricatorPolicyCapability::getCapabilityByKey($capability);
       if ($capobj) {
         $label = $capobj->getCapabilityName();
       } else {
         $label = pht('Capability "%s"', $capability);
       }
     }
 
     $this->setLabel($label);
 
     return $this;
   }
 
   protected function getCustomControlClass() {
     return 'aphront-form-control-policy';
   }
 
   protected function getOptions() {
     $capability = $this->capability;
+    $policies = $this->policies;
+
+    // Exclude object policies which don't make sense here. This primarily
+    // filters object policies associated from template capabilities (like
+    // "Default Task View Policy" being set to "Task Author") so they aren't
+    // made available on non-template capabilities (like "Can Bulk Edit").
+    foreach ($policies as $key => $policy) {
+      if ($policy->getType() != PhabricatorPolicyType::TYPE_OBJECT) {
+        continue;
+      }
+
+      $rule = PhabricatorPolicyQuery::getObjectPolicyRule($policy->getPHID());
+      if (!$rule) {
+        continue;
+      }
+
+      $target = nonempty($this->templateObject, $this->object);
+      if (!$rule->canApplyToObject($target)) {
+        unset($policies[$key]);
+        continue;
+      }
+    }
 
     $options = array();
-    foreach ($this->policies as $policy) {
+    foreach ($policies as $policy) {
       if ($policy->getPHID() == PhabricatorPolicies::POLICY_PUBLIC) {
         // Never expose "Public" for capabilities which don't support it.
         $capobj = PhabricatorPolicyCapability::getCapabilityByKey($capability);
         if (!$capobj || !$capobj->shouldAllowPublicPolicySetting()) {
           continue;
         }
       }
+
       $policy_short_name = id(new PhutilUTF8StringTruncator())
         ->setMaximumGlyphs(28)
         ->truncateString($policy->getName());
 
       $options[$policy->getType()][$policy->getPHID()] = array(
         'name' => $policy_short_name,
         'full' => $policy->getName(),
         'icon' => $policy->getIcon(),
       );
     }
 
     // If we were passed several custom policy options, throw away the ones
     // which aren't the value for this capability. For example, an object might
     // have a custom view pollicy and a custom edit policy. When we render
     // the selector for "Can View", we don't want to show the "Can Edit"
     // custom policy -- if we did, the menu would look like this:
     //
     //   Custom
     //     Custom Policy
     //     Custom Policy
     //
     // ...where one is the "view" custom policy, and one is the "edit" custom
     // policy.
 
     $type_custom = PhabricatorPolicyType::TYPE_CUSTOM;
     if (!empty($options[$type_custom])) {
       $options[$type_custom] = array_select_keys(
         $options[$type_custom],
         array($this->getValue()));
     }
 
     // If there aren't any custom policies, add a placeholder policy so we
     // render a menu item. This allows the user to switch to a custom policy.
 
     if (empty($options[$type_custom])) {
       $placeholder = new PhabricatorPolicy();
       $placeholder->setName(pht('Custom Policy...'));
       $options[$type_custom][$this->getCustomPolicyPlaceholder()] = array(
         'name' => $placeholder->getName(),
         'full' => $placeholder->getName(),
         'icon' => $placeholder->getIcon(),
       );
     }
 
     $options = array_select_keys(
       $options,
       array(
         PhabricatorPolicyType::TYPE_GLOBAL,
+        PhabricatorPolicyType::TYPE_OBJECT,
         PhabricatorPolicyType::TYPE_USER,
         PhabricatorPolicyType::TYPE_CUSTOM,
         PhabricatorPolicyType::TYPE_PROJECT,
       ));
 
     return $options;
   }
 
   protected function renderInput() {
     if (!$this->object) {
       throw new Exception(pht('Call setPolicyObject() before rendering!'));
     }
     if (!$this->capability) {
       throw new Exception(pht('Call setCapability() before rendering!'));
     }
 
     $policy = $this->object->getPolicy($this->capability);
     if (!$policy) {
       // TODO: Make this configurable.
       $policy = PhabricatorPolicies::POLICY_USER;
     }
 
     if (!$this->getValue()) {
       $this->setValue($policy);
     }
 
     $control_id = celerity_generate_unique_node_id();
     $input_id = celerity_generate_unique_node_id();
 
     $caret = phutil_tag(
       'span',
       array(
         'class' => 'caret',
       ));
 
     $input = phutil_tag(
       'input',
       array(
         'type' => 'hidden',
         'id' => $input_id,
         'name' => $this->getName(),
         'value' => $this->getValue(),
       ));
 
     $options = $this->getOptions();
 
     $order = array();
     $labels = array();
     foreach ($options as $key => $values) {
       $order[$key] = array_keys($values);
       $labels[$key] = PhabricatorPolicyType::getPolicyTypeName($key);
     }
 
     $flat_options = array_mergev($options);
 
     $icons = array();
     foreach (igroup($flat_options, 'icon') as $icon => $ignored) {
       $icons[$icon] = id(new PHUIIconView())
         ->setIconFont($icon);
     }
 
 
+    if ($this->templatePHIDType) {
+      $context_path = 'template/'.$this->templatePHIDType.'/';
+    } else {
+      $object_phid = $this->object->getPHID();
+      if ($object_phid) {
+        $context_path = 'object/'.$object_phid.'/';
+      } else {
+        $object_type = phid_get_type($this->object->generatePHID());
+        $context_path = 'type/'.$object_type.'/';
+      }
+    }
+
     Javelin::initBehavior(
       'policy-control',
       array(
         'controlID' => $control_id,
         'inputID' => $input_id,
         'options' => $flat_options,
         'groups' => array_keys($options),
         'order' => $order,
         'icons' => $icons,
         'labels' => $labels,
         'value' => $this->getValue(),
         'capability' => $this->capability,
+        'editURI' => '/policy/edit/'.$context_path,
         'customPlaceholder' => $this->getCustomPolicyPlaceholder(),
       ));
 
     $selected = idx($flat_options, $this->getValue(), array());
     $selected_icon = idx($selected, 'icon');
     $selected_name = idx($selected, 'name');
 
+    $spaces_control = $this->buildSpacesControl();
+
     return phutil_tag(
       'div',
       array(
       ),
       array(
+        $spaces_control,
         javelin_tag(
           'a',
           array(
             'class' => 'grey button dropdown has-icon policy-control',
             'href' => '#',
             'mustcapture' => true,
             'sigil' => 'policy-control',
             'id' => $control_id,
           ),
           array(
             $caret,
             javelin_tag(
               'span',
               array(
                 'sigil' => 'policy-label',
                 'class' => 'phui-button-text',
               ),
               array(
                 idx($icons, $selected_icon),
                 $selected_name,
               )),
           )),
         $input,
       ));
 
     return AphrontFormSelectControl::renderSelectTag(
       $this->getValue(),
       $this->getOptions(),
       array(
         'name'      => $this->getName(),
         'disabled'  => $this->getDisabled() ? 'disabled' : null,
         'id'        => $this->getID(),
       ));
   }
 
   private function getCustomPolicyPlaceholder() {
     return 'custom:placeholder';
   }
 
+  private function buildSpacesControl() {
+    if ($this->capability != PhabricatorPolicyCapability::CAN_VIEW) {
+      return null;
+    }
+
+    if (!($this->object instanceof PhabricatorSpacesInterface)) {
+      return null;
+    }
+
+    $viewer = $this->getUser();
+    if (!PhabricatorSpacesNamespaceQuery::getViewerSpacesExist($viewer)) {
+      return null;
+    }
+
+    $space_phid = $this->getSpacePHID();
+    if ($space_phid === null) {
+      $space_phid = $viewer->getDefaultSpacePHID();
+    }
+
+    $select = AphrontFormSelectControl::renderSelectTag(
+      $space_phid,
+      PhabricatorSpacesNamespaceQuery::getSpaceOptionsForViewer(
+        $viewer,
+        $space_phid),
+      array(
+        'name' => 'spacePHID',
+      ));
+
+    return $select;
+  }
+
 }
diff --git a/src/view/phui/PHUI.php b/src/view/phui/PHUI.php
index 8848b99e1..d3510a231 100644
--- a/src/view/phui/PHUI.php
+++ b/src/view/phui/PHUI.php
@@ -1,59 +1,59 @@
 <?php
 
-final class PHUI {
+final class PHUI extends Phobject {
 
   const MARGIN_SMALL = 'ms';
   const MARGIN_MEDIUM = 'mm';
   const MARGIN_LARGE = 'ml';
 
   const MARGIN_SMALL_LEFT = 'msl';
   const MARGIN_MEDIUM_LEFT = 'mml';
   const MARGIN_LARGE_LEFT = 'mll';
 
   const MARGIN_SMALL_RIGHT = 'msr';
   const MARGIN_MEDIUM_RIGHT = 'mmr';
   const MARGIN_LARGE_RIGHT = 'mlr';
 
   const MARGIN_SMALL_BOTTOM = 'msb';
   const MARGIN_MEDIUM_BOTTOM = 'mmb';
   const MARGIN_LARGE_BOTTOM = 'mlb';
 
   const MARGIN_SMALL_TOP = 'mst';
   const MARGIN_MEDIUM_TOP = 'mmt';
   const MARGIN_LARGE_TOP = 'mlt';
 
   const PADDING_SMALL = 'ps';
   const PADDING_MEDIUM = 'pm';
   const PADDING_LARGE = 'pl';
 
   const PADDING_SMALL_LEFT = 'psl';
   const PADDING_MEDIUM_LEFT = 'pml';
   const PADDING_LARGE_LEFT = 'pll';
 
   const PADDING_SMALL_RIGHT = 'psr';
   const PADDING_MEDIUM_RIGHT = 'pmr';
   const PADDING_LARGE_RIGHT = 'plr';
 
   const PADDING_SMALL_BOTTOM = 'psb';
   const PADDING_MEDIUM_BOTTOM = 'pmb';
   const PADDING_LARGE_BOTTOM = 'plb';
 
   const PADDING_SMALL_TOP = 'pst';
   const PADDING_MEDIUM_TOP = 'pmt';
   const PADDING_LARGE_TOP = 'plt';
 
   const TEXT_BOLD = 'phui-text-bold';
   const TEXT_UPPERCASE = 'phui-text-uppercase';
   const TEXT_STRIKE = 'phui-text-strike';
 
   const TEXT_RED = 'phui-text-red';
   const TEXT_ORANGE = 'phui-text-orange';
   const TEXT_YELLOW = 'phui-text-yellow';
   const TEXT_GREEN = 'phui-text-green';
   const TEXT_BLUE = 'phui-text-blue';
   const TEXT_INDIGO = 'phui-text-indigo';
   const TEXT_VIOLET = 'phui-text-violet';
   const TEXT_WHITE = 'phui-text-white';
   const TEXT_BLACK = 'phui-text-black';
 
 }
diff --git a/src/view/phui/PHUIPinboardItemView.php b/src/view/phui/PHUIPinboardItemView.php
index 704433271..93e227704 100644
--- a/src/view/phui/PHUIPinboardItemView.php
+++ b/src/view/phui/PHUIPinboardItemView.php
@@ -1,138 +1,153 @@
 <?php
 
 final class PHUIPinboardItemView extends AphrontView {
 
   private $imageURI;
   private $uri;
   private $header;
   private $iconBlock = array();
   private $disabled;
-
+  private $object;
   private $imageWidth;
   private $imageHeight;
 
   public function setHeader($header) {
     $this->header = $header;
     return $this;
   }
 
   public function setURI($uri) {
     $this->uri = $uri;
     return $this;
   }
 
   public function setImageURI($image_uri) {
     $this->imageURI = $image_uri;
     return $this;
   }
 
   public function setImageSize($x, $y) {
     $this->imageWidth = $x;
     $this->imageHeight = $y;
     return $this;
   }
 
   public function addIconCount($icon, $count) {
     $this->iconBlock[] = array($icon, $count);
     return $this;
   }
 
   public function setDisabled($disabled) {
     $this->disabled = $disabled;
     return $this;
   }
 
+  public function setObject($object) {
+    $this->object = $object;
+    return $this;
+  }
+
   public function render() {
     require_celerity_resource('phui-pinboard-view-css');
     $header = null;
     if ($this->header) {
       $header_color = null;
       if ($this->disabled) {
         $header_color = 'phui-pinboard-disabled';
       }
       $header = phutil_tag(
         'div',
         array(
           'class' => 'phui-pinboard-item-header '.$header_color,
         ),
-        phutil_tag('a', array('href' => $this->uri), $this->header));
+        array(
+          id(new PHUISpacesNamespaceContextView())
+            ->setUser($this->getUser())
+            ->setObject($this->object),
+          phutil_tag(
+            'a',
+            array(
+              'href' => $this->uri,
+            ),
+            $this->header),
+        ));
     }
 
     $image = null;
     if ($this->imageWidth) {
       $image = phutil_tag(
         'a',
         array(
           'href' => $this->uri,
           'class' => 'phui-pinboard-item-image-link',
         ),
         phutil_tag(
           'img',
           array(
             'src'     => $this->imageURI,
             'width'   => $this->imageWidth,
             'height'  => $this->imageHeight,
           )));
     }
 
     $icons = array();
     if ($this->iconBlock) {
       $icon_list = array();
       foreach ($this->iconBlock as $block) {
         $icon = id(new PHUIIconView())
           ->setIconFont($block[0].' lightgreytext')
           ->addClass('phui-pinboard-icon');
 
         $count = phutil_tag('span', array(), $block[1]);
         $icon_list[] = phutil_tag(
           'span',
           array(
             'class' => 'phui-pinboard-item-count',
           ),
           array($icon, $count));
       }
       $icons = phutil_tag(
         'div',
         array(
           'class' => 'phui-pinboard-icons',
         ),
         $icon_list);
     }
 
     $content = $this->renderChildren();
     if ($content) {
       $content = phutil_tag(
         'div',
         array(
           'class' => 'phui-pinboard-item-content',
         ),
         $content);
     }
 
     $classes = array();
     $classes[] = 'phui-pinboard-item-view';
     if ($this->disabled) {
       $classes[] = 'phui-pinboard-item-disabled';
     }
 
     $item = phutil_tag(
       'div',
       array(
         'class' => implode(' ', $classes),
       ),
       array(
         $image,
         $header,
         $content,
         $icons,
       ));
 
     return phutil_tag(
       'li',
       array(
         'class' => 'phui-pinboard-list-item',
       ),
       $item);
   }
 
 }
diff --git a/webroot/rsrc/js/application/herald/HeraldRuleEditor.js b/webroot/rsrc/js/application/herald/HeraldRuleEditor.js
index e7300528d..c94dd0d6c 100644
--- a/webroot/rsrc/js/application/herald/HeraldRuleEditor.js
+++ b/webroot/rsrc/js/application/herald/HeraldRuleEditor.js
@@ -1,372 +1,373 @@
 /**
  * @requires multirow-row-manager
  *           javelin-install
  *           javelin-util
  *           javelin-dom
  *           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() {
       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);
       }
 
       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':
         case 'taskpriority':
         case 'taskstatus':
         case 'legaldocuments':
         case 'applicationemail':
+        case 'space':
           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':
         case 'flagcolor':
         case 'value-ref-type':
         case 'value-ref-change':
           input = this._renderSelect(this._config.select[type].options);
           get_fn = function() { return input.value; };
           set_fn = function(v) { input.value = v; };
           set_fn(this._config.select[type]['default']);
           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) {
       var tokenizerConfig = {
         src : this._config.template.source[type].uri,
         placeholder: this._config.template.source[type].placeholder,
         browseURI: this._config.template.source[type].browseURI,
         icons : this._config.template.icons,
         username : this._config.username
       };
 
       var build = JX.Prefab.newTokenizerFromTemplate(
         this._config.template.markup,
         tokenizerConfig);
       build.tokenizer.start();
 
       return [
         build.node,
         function() {
           return build.tokenizer.getTokens();
         },
         function(map) {
           for (var k in map) {
             build.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);
     }
   }
 });
diff --git a/webroot/rsrc/js/application/maniphest/behavior-batch-editor.js b/webroot/rsrc/js/application/maniphest/behavior-batch-editor.js
index 5cf568ce2..ad2cf32d9 100644
--- a/webroot/rsrc/js/application/maniphest/behavior-batch-editor.js
+++ b/webroot/rsrc/js/application/maniphest/behavior-batch-editor.js
@@ -1,150 +1,158 @@
 /**
  * @provides javelin-behavior-maniphest-batch-editor
  * @requires javelin-behavior
  *           javelin-dom
  *           javelin-util
  *           phabricator-prefab
  *           multirow-row-manager
  *           javelin-json
  */
 
 JX.behavior('maniphest-batch-editor', function(config) {
   var root = JX.$(config.root);
   var editor_table = JX.DOM.find(root, 'table', 'maniphest-batch-actions');
   var manager = new JX.MultirowRowManager(editor_table);
   var action_rows = [];
 
   function renderRow() {
     var action_select = JX.Prefab.renderSelect(
       {
         'add_project': 'Add Projects',
         'remove_project' : 'Remove Projects',
         'priority': 'Change Priority',
         'status': 'Change Status',
         'add_comment': 'Comment',
         'assign': 'Assign',
         'add_ccs' : 'Add CCs',
-        'remove_ccs' : 'Remove CCs'
+        'remove_ccs' : 'Remove CCs',
+        'space': 'Shift to Space'
       });
 
     var proj_tokenizer = build_tokenizer(config.sources.project);
     var owner_tokenizer = build_tokenizer(config.sources.owner);
     var cc_tokenizer = build_tokenizer(config.sources.cc);
+    var space_tokenizer = build_tokenizer(config.sources.spaces);
 
     var priority_select = JX.Prefab.renderSelect(config.priorityMap);
     var status_select = JX.Prefab.renderSelect(config.statusMap);
     var comment_input = JX.$N('input', {style: {width: '100%'}});
 
     var cell = JX.$N('td', {className: 'batch-editor-input'});
     var vfunc = null;
 
     function update() {
       switch (action_select.value) {
         case 'add_project':
         case 'remove_project':
           JX.DOM.setContent(cell, proj_tokenizer.template);
           vfunc = function() {
             return JX.keys(proj_tokenizer.object.getTokens());
           };
           break;
         case 'add_ccs':
         case 'remove_ccs':
           JX.DOM.setContent(cell, cc_tokenizer.template);
           vfunc = function() {
             return JX.keys(cc_tokenizer.object.getTokens());
           };
           break;
         case 'assign':
           JX.DOM.setContent(cell, owner_tokenizer.template);
           vfunc = function() {
             return JX.keys(owner_tokenizer.object.getTokens());
           };
           break;
+        case 'space':
+          JX.DOM.setContent(cell, space_tokenizer.template);
+          vfunc = function() {
+            return JX.keys(space_tokenizer.object.getTokens());
+          };
+          break;
         case 'add_comment':
           JX.DOM.setContent(cell, comment_input);
           vfunc = function() {
             return comment_input.value;
           };
           break;
         case 'priority':
           JX.DOM.setContent(cell, priority_select);
           vfunc = function() { return priority_select.value; };
           break;
         case 'status':
           JX.DOM.setContent(cell, status_select);
           vfunc = function() { return status_select.value; };
           break;
       }
     }
 
     JX.DOM.listen(action_select, 'change', null, update);
     update();
 
     return {
       nodes : [JX.$N('td', {}, action_select), cell],
       dataCallback : function() {
         return {
           action: action_select.value,
           value: vfunc()
         };
       }
     };
   }
 
   function onaddaction(e) {
     e.kill();
     addRow({});
   }
 
   function addRow(info) {
     var data = renderRow(info);
     var row = manager.addRow(data.nodes);
     var id = manager.getRowID(row);
 
     action_rows[id] = data.dataCallback;
   }
 
   function onsubmit() {
     var input = JX.$(config.input);
 
     var actions = [];
     for (var k in action_rows) {
       actions.push(action_rows[k]());
     }
 
     input.value = JX.JSON.stringify(actions);
   }
 
   addRow({});
 
   JX.DOM.listen(
     root,
     'click',
     'add-action',
     onaddaction);
 
   JX.DOM.listen(
     root,
     'submit',
     null,
     onsubmit);
 
   manager.listen(
     'row-removed',
     function(row_id) {
       delete action_rows[row_id];
     });
 
   function build_tokenizer(tconfig) {
     var built = JX.Prefab.newTokenizerFromTemplate(
       config.tokenizerTemplate,
       JX.copy({}, tconfig));
     built.tokenizer.start();
 
     return {
       object: built.tokenizer,
       template: built.node
     };
   }
 
 });
diff --git a/webroot/rsrc/js/application/policy/behavior-policy-control.js b/webroot/rsrc/js/application/policy/behavior-policy-control.js
index ddbe9f17e..ea6676119 100644
--- a/webroot/rsrc/js/application/policy/behavior-policy-control.js
+++ b/webroot/rsrc/js/application/policy/behavior-policy-control.js
@@ -1,130 +1,130 @@
 /**
  * @provides javelin-behavior-policy-control
  * @requires javelin-behavior
  *           javelin-dom
  *           javelin-util
  *           phuix-dropdown-menu
  *           phuix-action-list-view
  *           phuix-action-view
  *           javelin-workflow
  * @javelin
  */
 JX.behavior('policy-control', function(config) {
   var control = JX.$(config.controlID);
   var input = JX.$(config.inputID);
   var value = config.value;
 
   var menu = new JX.PHUIXDropdownMenu(control)
     .setWidth(260)
     .setAlign('left');
 
   menu.listen('open', function() {
     var list = new JX.PHUIXActionListView();
 
     for (var ii = 0; ii < config.groups.length; ii++) {
       var group = config.groups[ii];
 
       list.addItem(
         new JX.PHUIXActionView()
           .setName(config.labels[group])
           .setLabel(true));
 
       for (var jj = 0; jj < config.order[group].length; jj++) {
         var phid = config.order[group][jj];
 
         var onselect;
         if (group == 'custom') {
           onselect = JX.bind(null, function(phid) {
             var uri = get_custom_uri(phid, config.capability);
 
             new JX.Workflow(uri)
               .setHandler(function(response) {
                 if (!response.phid) {
                   return;
                 }
 
                 replace_policy(phid, response.phid, response.info);
                 select_policy(response.phid);
               })
               .start();
 
           }, phid);
         } else {
           onselect = JX.bind(null, select_policy, phid);
         }
 
         var option = config.options[phid];
         var item = new JX.PHUIXActionView()
           .setName(option.name)
           .setIcon(option.icon + ' darkgreytext')
           .setHandler(JX.bind(null, function(fn, e) {
             e.prevent();
             menu.close();
             fn();
           }, onselect));
 
         if (phid == value) {
           item.setSelected(true);
         }
 
         list.addItem(item);
       }
     }
 
     menu.setContent(list.getNode());
   });
 
 
   var select_policy = function(phid) {
     JX.DOM.setContent(
       JX.DOM.find(control, 'span', 'policy-label'),
       render_option(phid));
 
     input.value = phid;
     value = phid;
   };
 
 
   var render_option = function(phid, with_title) {
     var option = config.options[phid];
 
     var name = option.name;
     if (with_title && (option.full != option.name)) {
       name = JX.$N('span', {title: option.full}, name);
     }
 
     return [JX.$H(config.icons[option.icon]), name];
   };
 
 
   /**
    * Get the workflow URI to create or edit a policy with a given PHID.
    */
   var get_custom_uri = function(phid, capability) {
-    var uri = '/policy/edit/';
+    var uri = config.editURI;
     if (phid != config.customPlaceholder) {
       uri += phid + '/';
     }
     uri += '?capability=' + capability;
     return uri;
   };
 
 
   /**
    * Replace an existing policy option with a new one. Used to swap out custom
    * policies after the user edits them.
    */
   var replace_policy = function(old_phid, new_phid, info) {
     config.options[new_phid] = info;
     for (var k in config.order) {
       for (var ii = 0; ii < config.order[k].length; ii++) {
         if (config.order[k][ii] == old_phid) {
           config.order[k][ii] = new_phid;
           return;
         }
       }
     }
   };
 
 
 });