prc-slack owns every Slack connection: production publish notices, watcher DMs, and PRC Nexus chat.
Intent gate
| Constructor | Intent | When it sends |
|---|---|---|
Message::notice() | Notice | Always on production. Never on local. Otherwise when Settings notices_enabled is true. |
Message::conversation() | Conversation | Any environment |
Message::intake() | Intake | Always except local, which follows Settings notices_enabled. |
Bot::send_notification() always builds a notice. Session replies always build a conversation. Bot::post_ask() builds an intake message for Help Center Asks. There is no public Message constructor, so chat cannot inherit the notice gate and notices cannot skip it.
Connection
Connection::resolve() is the only credential reader.
| Environment | Token / signing secret / workspace |
|---|---|
| production | VIP constants only. Site option overrides are ignored. |
| non-production | Non-empty option override wins per field; constants are the fallback. |
Constants:
| Constant | Use |
|---|---|
PRC_PLATFORM_SLACK_TOKEN | Bot token (xoxb-…) for chat.postMessage, reactions.add, chat.getPermalink, and agents.sessions.* |
PRC_PLATFORM_SLACK_SIGNING_SECRET | Events API HMAC |
PRC_PLATFORM_SLACK_WORKSPACE_ID | Team allowlist (T…) |
PRC_PLATFORM_ORIGIN_APP_ID | Origin app ID (app_…). JWT iss. |
PRC_PLATFORM_ORIGIN_APP_KEY_ID | Signing-key ID for JWT kid. Falls back to the app ID. |
PRC_PLATFORM_ORIGIN_APP_PRIVATE_KEY | PKCS#8 Ed25519 private key. Never store this in settings. |
PRC_PLATFORM_ORIGIN_INSTALLATION_ID | Installation ID (i_…) used to mint oit_… tokens. |
Connection never returns secrets as strings. Callers ask it to verify a signature or deliver a Message. Origin credentials live only in VIP constants.
Publish notifications
- Consumes
prc_platform_async_on_publishfromprc-post-publish-pipeline. - Skips forked posts (
_prc_fork_parentmeta). - Routes
decodedposts to#decoded; other public post types use#publish(channel_for_post()). - Message body includes post type label, title, permalink, Bit.ly shortlink when available, and bylines when
prc-staff-bylinesis active. - Sends when
Intent::Noticeis permitted for the current environment (production always, local never, other hosts whennotices_enabledis on).
Platform Factory pipeline
Factory\Factory owns the Cursor Origin webhook route, Action Scheduler jobs, and Slack merge actions. It does not add event branches to Bot::send_notification().
Factory notification flow
The pipeline uses these types:
| Type | Responsibility |
|---|---|
Origin_Delivery | Holds a payload after Ed25519 verification and the five-minute replay check. |
Pull_Event | Parses supported webhook payloads into pull request, author, reviewer, and approver values. |
Git_Ref | Removes one refs/heads/ prefix before branch comparison. |
Factory_Kind | Defines the audience, message copy, and merge-button rule for each notification. |
Factory_Roster | Resolves an Origin email or configured login mapping to a Slack member ID. |
Merge_Action | Encodes the backport pull request and authorized Slack recipient in the button value. |
The webhook handler claims webhook-id with wp_cache_add() and also requests a unique Action Scheduler job. Repeated deliveries converge on one queued job while the object-cache entry exists.
For a created pull request, the title suffix [backport] is enough to classify a backport. When Origin API credentials exist, Origin_Api fetches the pull request and adds its labels and description. The exact backport label also classifies the event. The description line Original author: @login selects the direct-message recipient; the pull request author is the fallback.
The merge button is limited to its direct-message recipient and is omitted while the pull request is a draft. A valid click replaces the button with a merging status and queues prc_slack_factory_merge. The job rejects drafts, tries Origin POST .../pulls/{n}/merge, and uses GitHub PUT .../pulls/{n}/merge for a mirrored or unsupported repository. An already merged pull request is a success. A failed job replaces the merge button with a link to the pull request.
Origin_Api signs a five-minute EdDSA app JWT with iss set to the app id and aud set to origin-apps, mints an installation token, and caches the token until shortly before expiry. The private key comes only from VIP configuration.
Register the Origin app
WordPress is the Cursor Origin app. Create the app in Origin, store the private key in VIP, then turn on factory notifications in WordPress.
Official reference: Origin API and Codebase settings. Manage apps at cursor.com/codebase/settings/apps.
-
Create the Origin app
Open codebase app settings. Create an internal app for Platform Factory. Copy the app ID (
app_…). -
Generate a signing key
Create a PKCS#8 Ed25519 pair locally. Upload only the public key. Keep the private key in a secrets manager. If Origin shows a separate signing-key ID, copy it; otherwise the app ID is the JWT
kid. -
Set the webhook and events
Copy Origin webhook URL from Settings → PRC Slack on the target WordPress site. Register that HTTPS URL on the app. Subscribe to
pull_request.created,pull_request.reviewer.added,pull_request.reviewer.rerequested,pull_request.review.submitted, andpull_request.merged. -
Install the app
Install the app on the Pew Research codebase. Approve
repository:pull_requests:readandrepository:contents:writefor the platform repository. Copy the installation ID (i_…). -
Store VIP constants
Create Platform Secrets items for the four
PRC_PLATFORM_ORIGIN_*names. Tag the target VIP environments. Sync withnpm run sync:vip-env-vars -- production --dry-run, then apply. Constants load on the next deploy. -
Configure Slack, then enable
Invite the Slack bot to the private engineering channel. Store the
C…channel ID, fill the roster, and keep bot logins. Confirm Settings shows Origin API credentials as Configured. Then turn on Enable Platform Factory notifications.
Signing key
Create the pair with OpenSSL:
openssl genpkey -algorithm ED25519 -out origin-app-private.pem
openssl pkey -in origin-app-private.pem -pubout -out origin-app-public.pem
Paste the public PEM (-----BEGIN PUBLIC KEY-----) when Origin asks for a signing key. WordPress signs app JWTs with the matching private key. Lifetime is about five minutes. Claims use iss = app ID and aud = origin-apps.
If a VIP environment variable cannot store raw newlines, store the PEM with literal \n sequences. PHP expands those before it reads the key.
Installation and scopes
Install from the app settings page, or send a workspace admin to:
https://cursor.com/codebase/apps/install?client_id=APP_ID&scope=repository:pull_requests:read%20repository:contents:write
repository:metadata:read is added automatically. repository:pull_requests:read lets WordPress load labels and the original-author line. repository:contents:write lets Origin merge a native pull request. Mirrored repositories still use the GitHub squash fallback and PRC_PLATFORM_GITHUB_TOKEN.
After approval, copy the installation ID (i_…). Partner-initiated installs put that ID in the sub claim of the installation_receipt JWT. An install from the settings UI also shows the ID on the installation record.
Select the platform repository. Do not grant every repository unless that is an explicit ops decision.
VIP constants
Origin VIP constants
-
PRC_PLATFORM_ORIGIN_APP_IDstring required -
Origin app ID (app_…). Used as JWT iss.
-
PRC_PLATFORM_ORIGIN_APP_KEY_IDstring -
Signing-key ID for JWT kid. Leave empty to use the app ID.
-
PRC_PLATFORM_ORIGIN_APP_PRIVATE_KEYstring required -
PKCS#8 Ed25519 private PEM. Never commit this value.
-
PRC_PLATFORM_ORIGIN_INSTALLATION_IDstring required -
Installation ID (i_…) used to mint oit_ tokens.
No fields match this filter.
Create one API Credential item per constant in the Platform Secrets vault. Title each item with the constant name. Tag production (and alpha, beta, or canary only when that host should call Origin). Preview, then apply:
npm run sync:vip-env-vars -- production --dry-run
npm run sync:vip-env-vars -- production
Remote env var changes take effect on the next deploy. After deploy, Settings → PRC Slack should show Origin API credentials: Configured.
Webhook signature checks use Origin's public JWKS, not this private key. The key is required to fetch pull request details and to merge from Slack.
Slack settings after credentials
- Invite the existing PRC Slack bot to the private engineering channel.
- Copy the channel ID (
C…) into Engineering channel ID. - Keep Trunk branch as
trunkunless Origin uses another default branch. - Keep bot logins such as
github-actions[bot]andprc-github-actions[bot]. - Add one
login = emailorlogin = U…line per engineer who should receive DMs when Origin has no email. - Turn on Enable Platform Factory notifications last.
On hosted staging, also turn on Enable publish notices. Factory messages use the same notice gate. Local PHP never posts them.
Verify
- Confirm Settings shows the production webhook URL and Configured credentials.
- Use Origin Ping Webhook (
POST /v1/origin/app/webhook/pings) with an app JWT. WordPress should return HTTP 200{ "ok": true }and queue no factory work forping. - Open a test pull request. The author should receive a DM. A trunk merge should also post to the engineering channel.
- Open a non-draft backport. The original author should see a merge button. A failed merge becomes a GitHub pull request link.
If DMs are missing, check the roster and bot-login list. If Settings still says Not configured, the deploy has not loaded the four VIP constants.
Announcement meta
| Meta key | Value | Policy |
|---|---|---|
_prc_slack_announcement | { channel, ts } | Last-write-wins on successful anchored publish |
When Slack returns ok: true but omits a usable ts, delivery still counts as success (Posted_Message with is_anchored() === false) and the announcement is not stored. Retrying would duplicate a delivered message.
Posted_Message::anchor() builds the same type for Nexus thread replies.
First-day stats follow-up
Twenty-four hours after an anchored publish notice, Action Scheduler runs prc_slack_first_day_stats. The job replies in the same Slack thread with Parse.ly views, visitors, average engaged time, and the top 10 referrers for the post's first 24 hours (post_date_gmt through plus one day).
Skip:
- Email CPTs (
prc_email_campaign,prc_email_txn) — campaigns get a Mailchimp-sent thread reply, then first-day Mailchimp stats 24 hours later (owned by email-builder) - Forks and
chart - Environments where notices are not permitted — the job is only scheduled after a successful notice, and the handler uses the same
Intent::Noticegate - Unanchored Slack delivery — no
ts, so no thread - Missing Parse.ly credentials — log and stop (no retry loop)
Source: includes/class-first-day-stats.php. Ops can run wp prc-slack first-day-stats <post_id> on production without waiting 24 hours.
| Meta key | Value | Policy |
|---|---|---|
_prc_slack_first_day_stats_ts | Announcement ts already reported | Idempotent per announcement |
Helpers for other plugins
Defined in includes/utils.php:
| Function | Use |
|---|---|
PRC\Platform\Slack\send_notification( $args ) | Ad-hoc channel post; optional reply_to; optional to_user |
PRC\Platform\Slack\notify_in_post_thread( $post_id, $args ) | Reply under the post's recorded announcement |
PRC\Platform\Slack\channel_for_post( $post ) | Resolve default channel |
Return type of send_notification() is Posted_Message|WP_Error|null. Treat a Posted_Message (including unanchored) as success. to_user is additive; existing callers keep passing channel.
PRC Nexus
One WordPress agent slug: prc-nexus. Other plugins register a PRC\Platform\Slack\Nexus\System on prc_nexus_systems. The registry compiles:
wp_register_agent( 'prc-nexus' )config (prompt, tools, action policy)- Slack app manifest fragments (
agent_view, suggested prompts, events, scopes) - Per-turn
tool_policyfor the Slack-mapped WordPress user (prc_slack_user_id)
Turns run as prc_wp_bot. Ability permission_callbacks cannot see the editor. Narrowing tools on the turn is the authorization point. Users without a Slack member mapping get an empty tool list.
Registered systems:
- Trending News (
prc-agents-trending-news) — workflow and analysis abilities. No Slack client. - Social Builder (
prc-social-builder) —prc-social-builder/generate-social-copyonly. Does not create social-package posts. The Social Builder AI feature must be enabled so the ability exists.
Inbound sessions
Events API payloads parse into a closed set: Turn_Requested, Session_Stopped, Session_Renamed, Surface_Opened, Ignored, plus Url_Verification.
A turn job (prc_slack_nexus_turn) sets agents.sessions.setStatus to processing, then active. Slack does not return the session to active on its own. The first turn with an empty stored title also calls agents.sessions.rename with the first line of the editor's message.
Turns prefer Action Scheduler. When as_enqueue_async_action() does not return a numeric id greater than 0, Nexus_Channel falls through to WP_Agent_Channel::receive() (WP-Cron or a synchronous handle()). That keeps Slack retries from looking like duplicates while still producing a reply. Turn_Gate::release() still runs if receive() throws.
A failed agents/chat turn still returns a well-formed array with an empty reply. Nexus_Channel reads metadata.agents_api.run_outcome and posts that failure to the thread instead of a generic “could not generate a response”.
Stop (agent_session_stopped) bumps the session generation, cancels agents/cancel-chat-run, unschedules a queued Action Scheduler job, and discards a late reply.
Committed manifests: plugins/prc-slack/manifest.staging.yml (alpha.pewresearch.org/pewresearch-org) and manifest.production.yml (www.pewresearch.org). Release workflows update the live Slack apps from those files (see RELEASING.md). wp prc-slack manifest prints the compiled JSON with home_url(). A unit test asserts the YAML still lists agent_view, agent_session_stopped, and those request URLs.
Stub REST route
Bot::register_endpoint() defines GET /wp-json/prc-api/v3/slackbot/interact for future interactive callbacks. Source marks it @TODO: Wire this into the loader via rest_api_init. The route is not active by default. Do not depend on it.
Dependencies
| Dependency | Type | Notes |
|---|---|---|
prc-post-publish-pipeline | Required plugin | Provides prc_platform_async_on_publish |
agents-api | Soft | Nexus chat; notices still send without it |
prc-staff-bylines | Optional | Appends byline strings when active |