# 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 <PR branch>
installDependencies() → yarn/npm install (non-blocking, failure is a warning)
```

Repos live in `workdir/<repoName>/`.

#### 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
```
<comment text, max 4000 chars>
```

## 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
<PR diff, max 12000 chars>
```
```

#### 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 <prompt> --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(<repoName>): address PR comment #<commentId>"
git push origin <branch>
```

#### 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
