Skip to main content

Community PR Processing Deep Dive

The community PR system enables works to accept contributions from external users via GitHub pull requests. When community members submit PRs to a work's main repository, the system automatically extracts new items, adds them to the data repository, and provides feedback on the PR.

Architecture

The system is implemented in packages/agent/src/community-pr/ with a single service:

  • CommunityPrProcessorService -- processes open PRs across all community-enabled works.

Dependencies:

  • GitFacadeService -- lists PRs, reads file changes, creates comments, closes PRs
  • AiFacadeService -- extracts structured item data from PR diffs
  • WorkRepository -- queries works with community PR enabled, persists state

Processing Flow

1. Work Discovery

processAllWorks()
-> findWithCommunityPrEnabled()
-> for each work: processWork()

The scheduler (via Trigger.dev or BullMQ) periodically calls processAllWorks(), which loads all works with community PR processing enabled.

2. PR Enumeration

For each work, the service:

  1. Resolves the work owner and main repository name.
  2. Lists open PRs via gitFacade.listPullRequests() (up to 100 per batch).
  3. Filters out already-processed PRs using state.processedPrNumbers (a Set for O(1) lookups).

3. Single PR Processing

For each unprocessed PR, processSinglePr() runs:

Step 1: Extract Change Context

const files = await this.gitFacade.getPullRequestFiles(owner, mainRepo, pr.number, gitOptions);

File patches are concatenated into a change context string, capped at 50,000 characters to prevent prompt overflow:

--- path/to/file.md (added) ---
+ ## New Tool
+ - [ToolName](https://example.com) - Description of the tool

If no meaningful changes are found, the system posts a comment explaining this and skips the PR.

Step 2: Clone Data Repository

The data repository is cloned/pulled to get the current state, including existing categories for context.

Step 3: AI Item Extraction

The change context is sent to the AI with a structured extraction prompt:

You are analyzing a community pull request submitted to the "{workName}" work.
Work description: {description}
Existing categories: {categoryNames}
PR Title: {prTitle}
PR Changes: {changeContext}

Extract all new items being proposed in this PR...

The AI returns a structured response validated against a Zod schema:

const extractedItemSchema = z.object({
items: z.array(
z.object({
name: z.string(),
description: z.string(),
source_url: z.string(),
category: z.string(),
tags: z.array(z.string())
})
)
});

If zero items are extracted, the system posts a comment and skips.

Step 4: Write to Data Repository

For each extracted item:

  1. Create the item work in the data repo.
  2. Write the item.yml metadata file.
  3. Write a markdown description file.

Step 5: Commit and Push

await this.gitFacade.add(work.gitProvider, dest, '.');
await this.gitFacade.commit(work.gitProvider, dest, `Add ${items.length} item(s) from community PR #${pr.number}`);
await this.gitFacade.push({ dir: dest }, gitOptions);

Step 6: Comment and Optionally Close

The service posts a comment listing all added items:

Thank you for your contribution! The following items have been added:
- Tool A
- Tool B
The data repository has been updated automatically.

If autoClose is enabled on the work, the PR is closed after processing.

State Management

Each work maintains a CommunityPrState object:

interface CommunityPrState {
processedPrNumbers: number[]; // PR numbers already processed
totalItemsAdded: number; // Cumulative items added
lastProcessedAt?: string; // ISO timestamp of last processing
lastError?: string; // Most recent error message
}

Bounded Growth

The processedPrNumbers array is capped at 500 entries to prevent unbounded growth:

if (state.processedPrNumbers.length > MAX_PROCESSED_PR_NUMBERS) {
state.processedPrNumbers = state.processedPrNumbers.slice(-MAX_PROCESSED_PR_NUMBERS);
}

Old PR numbers are trimmed from the start since they are unlikely to reappear as open PRs.

State Persistence

After processing all unprocessed PRs for a work, the updated state is saved:

await this.workRepository.update(work.id, { communityPrState: state });

Item counts are atomically incremented:

await this.workRepository.increment(work.id, 'itemsCount', totalItemsAdded);

Error Handling

Errors are handled at two levels:

  1. Per-work: If processing a work fails entirely, the error is logged and added to the result's errors array. Other works continue processing.
  2. Per-PR: If a single PR fails, the error is logged and stored in state.lastError. The PR number is still added to processedPrNumbers to avoid reprocessing a broken PR.

Configuration

Community PR processing is controlled by two work settings:

SettingTypeDescription
communityPrEnabledbooleanEnable/disable community PR processing
communityPrAutoClosebooleanAutomatically close PRs after processing

Limits and Constants

ConstantValuePurpose
MAX_PROCESSED_PR_NUMBERS500Cap on tracked processed PR numbers
MAX_CHANGE_CONTEXT_LENGTH50,000Maximum characters of PR diff sent to AI

Batch Processing Results

The processAllWorks() method returns an aggregate result:

interface CommunityPrProcessingResult {
processed: number; // Total items added across all works
errors: Array<{ workId: string; error: string }>; // Failed works
}