# GitHub PR Comment Pipeline Pipeline that detects comments on bot-authored PRs and automatically responds using a Cursor agent. If the comment requests a code change, the agent applies it, commits, and pushes. --- ## Overview ``` GitHub PRs │ ▼ PrCommentPoller ─── every 60s ───► lists bot's PRs │ filters comments │ enqueues the most recent one ▼ JobQueue (concurrency 1) │ ▼ PrCommentPipeline ├── checkout branch ├── install deps ├── fetch PR diff ├── runAgent (Cursor CLI) ─── REPLY_START...REPLY_END ├── post reply on GitHub └── commit + push (if there were changes) ``` --- ## 1. Poller (`PrCommentPoller`) **File:** `src/poller/pr-comment-poller.ts` **Interval:** 60 seconds (`POLLER.PR_COMMENT_INTERVAL_MS`) ### What it does each cycle 1. **Guard conditions** — skips the cycle if: - A poll is already in progress (`this.polling = true`) - The queue is busy (`queue.isBusy()`) - A Jira job is running (`queue.getCurrentJobKind() === "jira"`) 2. **Iterates all repos** registered in `repos.json`. 3. For each repo, lists all open PRs via `listOpenPrs()`. 4. **Filters only bot PRs** (`pr.author === githubBotUser`, e.g. `"hu-agent[bot]"`). 5. For each bot PR, collects **two types of comments**: | Type | API | `isReviewComment` | |---|---|---| | Conversation (issue comment) | `listIssueComments()` | `false` | | Inline code (review comment) | `listPullReviewComments()` | `true` | 6. Sorts all comments by date descending and takes the **most recent** one. 7. If the last comment **is from the bot itself**, it is ignored (prevents replying to its own responses). 8. Creates a `PrCommentJobPayload` with the comment data and **enqueues** it. ### Enqueued payload ```typescript { jobKind: "pr_comment", repoName: string, // e.g. "humand-mobile" prNumber: number, // e.g. 6971 branch: string, // e.g. "fix/SQDP-3507-..." commentId: number, // ID of the comment to reply to commentBody: string, // Comment text isReviewComment: boolean, prUrl?: string, } ``` > **Current limitation:** only the most recent comment per PR is processed per cycle. If there are multiple unanswered comments, they are handled one at a time across polls. --- ## 2. Queue **Concurrency:** 1 (one job at a time, configured in `QUEUE.CONCURRENCY`). **Max retries:** 2 (`QUEUE.MAX_RETRIES`). **Job timeout:** 35 minutes (`QUEUE.JOB_TIMEOUT_MS`). When a `pr_comment` job is dequeued, it is passed to `PrCommentPipeline.execute()`. --- ## 3. Pipeline (`PrCommentPipeline`) **File:** `src/pipeline/pr-comment-pipeline.ts` ### Steps in order #### 3.1 Validate repo Looks up the repo in the registry. If not found, aborts with `success: false`. #### 3.2 Prepare local repo ``` ensureCloned() → clones if not present in workdir/ checkoutBranch() → git checkout installDependencies() → yarn/npm install (non-blocking, failure is a warning) ``` Repos live in `workdir//`. #### 3.3 Fetch the PR diff Calls `githubClient.getPrDiff(prNumber)` to get the unified diff. If it fails, execution continues — the diff is optional context for the agent. #### 3.4 Build the prompt (`buildPrCommentPrompt`) **File:** `src/agent/prompts.ts` The prompt has this structure: ``` # PR comment response You are responding to a comment on a pull request in this repository. **Repository:** humand-mobile **Branch:** fix/SQDP-3507-... ## Comment to respond to ``` ``` ## Your task 1. Understand what the commenter is asking or suggesting. 2. Analyze the codebase if needed (you are in the repo at the PR branch). 3. If the comment requests a code change, make the change. 4. Write a concise reply to the comment (explain what you did or answer their question). Put your **reply text** in a block that starts with REPLY_START and ends with REPLY_END... ## PR diff (for context) ```diff ``` ``` #### 3.5 Run the agent (`CursorCliClient.runAgent`) **File:** `src/agent/cursor-cli-client.ts` **Timeout:** 15 minutes (`CURSOR_AGENT.FOLLOWUP_TIMEOUT_MS`) Runs `cursor agent --yolo --print --trust --output-format stream-json` in the repo directory. The process inherits the bot's environment but with these variables overridden to prevent git from opening any GUI editor (e.g. VS Code) during operations like `rebase --continue`: ``` GIT_EDITOR=true GIT_SEQUENCE_EDITOR=true GIT_MERGE_AUTOEDIT=no GIT_TERMINAL_PROMPT=0 ``` The agent has access to all Cursor tools (read files, grep, edit, shell, etc.) and can explore the repo freely. #### 3.6 Parse the response (`parsePrCommentReply`) Looks for the `REPLY_START ... REPLY_END` block in the agent output. If the block is not found, falls back to the full output truncated to 4000 chars. If no reply text is found, aborts with `success: false`. #### 3.7 Post the reply on GitHub Depends on the comment type: | `isReviewComment` | Method | GitHub API | |---|---|---| | `true` | `createReviewCommentReply(prNumber, commentId, text)` | `POST /repos/.../pulls/{pull_number}/comments/{comment_id}/replies` | | `false` | `createIssueComment(prNumber, text)` | `POST /repos/.../issues/{issue_number}/comments` | #### 3.8 Commit and push (if there were changes) If the agent modified files in the repo: ``` git add -A git commit -m "chore(): address PR comment #" git push origin ``` #### 3.9 Clean stale branches Calls `cleanStaleBranches(baseBranch)` to delete local branches that no longer exist on the remote. --- ## 4. GitHub Client (`GitHubClientImpl`) **File:** `src/github/client.ts` Methods relevant to this pipeline: | Method | Description | |---|---| | `listOpenPrs()` | Lists open PRs with author and branch | | `listIssueComments(prNumber)` | PR conversation comments | | `listPullReviewComments(prNumber)` | Inline code comments (includes `in_reply_to_id`) | | `getPullReviewComment(commentId)` | A single review comment by ID | | `getPrDiff(prNumber)` | Unified diff of the PR | | `createIssueComment(prNumber, body)` | Posts a reply in the PR conversation | | `createReviewCommentReply(prNumber, commentId, body)` | Replies in a review comment thread | Authentication uses a **GitHub App token** obtained via `getGitHubToken()`, which refreshes the JWT token automatically. --- ## 5. Comment types and how they are replied to GitHub distinguishes two types of PR comments: ### Issue comments (conversation) - Appear in the "Conversation" tab of the PR - Created with `POST /issues/{number}/comments` - **Replied to with another issue comment** (no native thread reply) ### Review comments (inline) - Anchored to a specific line in the diff - Can form threads (identified by `in_reply_to_id`) - **Replied to with** `POST /pulls/{number}/comments/{comment_id}/replies` --- ## 6. Reference logs A full successful cycle looks like this in the logs: ``` pr_comment_poller_started → poller started job_received → comment detected and enqueued pr_comment_pipeline_started → pipeline started node_version_resolved → Node version for the repo branch_checked_out → PR branch checked out dependencies_installed → yarn install cursor_cli_started → agent running cursor_cli_finished → agent finished (exitCode: 0) review_comment_reply_created → reply posted on GitHub pr_comment_replied → pipeline confirmation committed_and_pushed → (if code changes were made) stale_branches_cleaned → local branch cleanup pr_comment_pipeline_completed job_completed ``` --- ## 7. Abort conditions The pipeline finishes with `success: false` if: - The repo is not in the registry - The agent produces no reply text (no `REPLY_START...REPLY_END` and empty output) - Posting the reply on GitHub fails (e.g. missing token permissions, closed PR) - The agent exceeds the 15-minute timeout - Any unhandled exception during execution