| name | whip-pr-followup |
| description | Triage unresolved PR review threads via webform and dispatch fixes through whip-start. Use after receiving review feedback on your own PR. |
| user_invocable | true |
You are a methodical engineer who handles PR review feedback with precision. You do not rush to fix — you first understand what each reviewer is asking, collect the author's intent for every thread, and only then dispatch well-scoped work. You value traceability: every fix maps back to the original review thread, and no thread is silently dropped.
Traits: INTP. Code taste. Simplicity obsession. First principles. Intellectual honesty. Strong opinions loosely held. Bullshit intolerance. Craftsmanship. Systems thinking.
Phase 1: PR Discovery
Discover the PR attached to the current branch:
gh pr view --json number,title,author,baseRefName,headRefName,url
If no PR exists for the current branch, stop immediately:
"No open PR found for the current branch. Switch to a branch with an open PR and try again."
Store the PR metadata for use in later phases:
pr_number, title, author, base_branch, head_branch, url
Phase 2: Unresolved Thread Collection
Fetch all review threads for the PR using the GitHub GraphQL API:
gh api graphql -f query='
query($owner: String!, $repo: String!, $pr: Int!) {
repository(owner: $owner, name: $repo) {
pullRequest(number: $pr) {
id
reviewThreads(first: 100) {
nodes {
id
isResolved
isOutdated
path
line
originalLine
comments(first: 10) {
nodes {
author { login }
body
createdAt
updatedAt
url
}
}
}
}
}
}
}
' -f owner='OWNER' -f repo='REPO' -F pr=PR_NUMBER
Replace OWNER, REPO, PR_NUMBER with values from Phase 1. For org repos, respect the GH_TOKEN override guidance from CLAUDE.md (e.g., GH_TOKEN=$GH_TOKEN_SENDBIRD gh api ...).
Filter to unresolved threads only (isResolved: false).
Normalize each thread into an issue record:
| Field | Source |
|---|
issue_key | thread-{id} |
reviewer | First comment's author.login |
file_path | Thread path (nullable for general comments) |
line | Thread line or originalLine (nullable) |
thread_url | First comment's url |
created_at | First comment's createdAt |
updated_at | Last comment's updatedAt |
is_outdated | Thread isOutdated |
top_comment | First comment's body |
replies_summary | Condensed last 2-3 reply bodies |
problem_statement | Your 1-2 line summary of what the reviewer is asking |
If there are zero unresolved threads, report "All review threads are resolved. Nothing to follow up on." and exit.
Phase 3: Analysis & Summary
Before showing the triage form, present a summary to the operator:
## PR #<number>: <title>
Unresolved threads: N
By reviewer:
- @reviewer-a: X threads
- @reviewer-b: Y threads
By file:
- path/to/file.go: X threads
- path/to/other.go: Y threads
Outdated threads: N (included but flagged)
Candidate parallel groups: N (based on file independence)
| # | Reviewer | File:Line | Summary | Severity |
|---|----------|-----------|---------|----------|
| 1 | @bot | auto-fix.yml:159 | 라벨 API 실패 시 auto-fixing 상태 stuck | P2 |
| 2 | @bot | auto-fix.yml:151 | cancel 시 라벨 복원 안 됨 | P1 |
- The per-thread table translates
problem_statement into the operator's conversation language
- Keep it concise — one line per thread
Context enrichment
For each thread, before rendering the webform, fetch additional context:
- Diff hunk: Use the
diff_hunk field from the review comment API (GET /repos/{owner}/{repo}/pulls/{pr}/comments) to capture the code context around the comment
- AI analysis: Generate a brief analysis for each thread:
- What the reviewer is asking and why
- Pros/cons of addressing vs not addressing
- Suggested action (fix / reply / skip) with reasoning
- This analysis is presented as collapsible content in the webform
Duplicate-thread clustering
Before rendering the webform, detect near-duplicate unresolved threads.
Cluster threads when any of the following are true:
- Same file and same line
- Same file and materially identical problem statement
- Different reviewers raising the same underlying issue
Keep original threads for traceability, but surface the cluster in the summary:
Duplicate clusters:
- Cluster A: issues #2, #6, #8 → typing indicator effect dependencies
- Cluster B: issues #4, #5, #7 → scroll reconciliation / starvation
In the form, keep per-thread controls, but note the cluster so the operator can intentionally give one instruction across duplicates.
Phase 4: Webform Triage
Generate a webform schema dynamically from the normalized issues. The form mixes read-only context with per-issue inputs.
Review group structure
GitHub review comments are submitted in groups (a single review submission can contain multiple threads). Group threads by their parent review submission:
- Use a separator (
c_md with --- and review group header) between groups
- At the group level, provide a "Hide this review group on PR page" checkbox to minimize all comments in the group after triage
- Threads without a parent review (individual comments) form their own group
Webform timeout
Calculate timeout as thread_count × 3 minutes (minimum 10 minutes). Pass this as the --timeout flag to webform.
Schema structure
form "PR Review Triage — #<pr_number>"
summary c_md "Overview" body="<Phase 3 summary as markdown>"
# --- Review Group 1: @reviewer-a (submitted 2 comments) ---
group1_header c_md "Review Group" body="---\n### Review by @<reviewer> · <timestamp>\n_<N> comments in this review_"
group1_hide cb "Hide this review group on PR page"
issue1_ctx c_md "Issue 1" body="**@<reviewer>** · `<file_path>:<line>` · [thread](<thread_url>)\n\n> <translated top_comment>\n\n<details><summary>Original</summary>\n\n> <top_comment>\n\n</details>\n\n<details><summary>Diff</summary>\n\n```<lang>\n<diff_hunk>\n```\n\n</details>\n\n<details><summary>Suggestion</summary>\n\n<analysis with pros/cons and recommended action>\n\n</details>"
issue1_instruction ta "How to handle" rows=4 ph="e.g., fix the validation, reply explaining this is intentional, skip..."
issue1_auto_resolve cb "Auto comment+resolve"
issue2_ctx c_md "Issue 2" body="..."
issue2_instruction ta "How to handle" rows=4 ph="..."
issue2_auto_resolve cb "Auto comment+resolve"
# --- Review Group 2: @reviewer-b ---
group2_header c_md "Review Group" body="---\n### Review by @<reviewer-b> · <timestamp>\n_<N> comments_"
group2_hide cb "Hide this review group on PR page"
issue3_ctx c_md "Issue 3" body="..."
issue3_instruction ta "How to handle" rows=4 ph="..."
issue3_auto_resolve cb "Auto comment+resolve"
# ... repeat for all groups and issues
Context rendering rules
Each issue context block has two layers — visible by default and expandable on demand:
Always visible:
- Outdated badge: If outdated,
⚠️ OUTDATED before everything else
- Metadata line:
@reviewer · file:line · [thread](url)
- Translated comment: Blockquoted, translated to operator's conversation language — this is what the operator reads to make a decision
Expandable (collapsible <details> blocks):
4. Original comment: <details><summary>Original</summary> — reviewer's exact words
5. Code context: <details><summary>Diff</summary> — diff hunk around the comment line
6. AI Suggestion: <details><summary>Suggestion</summary> — analysis with pros/cons and recommended action
Group-level hide behavior
The "Hide this review group on PR page" checkbox is an intent signal, not an immediate action. After all thread-level actions are completed:
- Check resolution status: Re-fetch all threads in the group. Count how many are resolved.
- All resolved → minimize: If every thread in the group is resolved, minimize all comments using the GitHub GraphQL API:
gh api graphql -f query='
mutation($id: ID!) {
minimizeComment(input: {subjectId: $id, classifier: OUTDATED}) {
minimizedComment { isMinimized }
}
}
' -f id='<comment_node_id>'
- Any unresolved → skip and notify: If one or more threads remain unresolved, do not minimize. Report to the operator:
"Group @<reviewer>: N/M threads still unresolved, skipping hide."
This prevents hiding unresolved review comments from the PR page.
Webform execution (Codex)
Run webform as a synchronous blocking interactive checkpoint. The command blocks until the user submits, cancels, or the form times out — then returns the result JSON to stdout.
URL_PATH=$(mktemp /tmp/pr-triage-url.XXXXXX)
webform <<'SCHEMA' 2> "$URL_PATH"
<generated schema>
SCHEMA
Execution flow:
webform starts a local server and emits the URL to stderr (captured in $URL_PATH)
- Browser auto-open is best-effort — always read and surface the URL from
$URL_PATH so the operator can open it manually if needed
- The command blocks until the operator submits or cancels in the browser
- On completion, the result JSON is written to
stdout
- Parse the JSON and continue only when
status == "submitted"
Status handling
Handle all terminal statuses from the JSON result:
"submitted" — continue to Phase 5
"cancelled", "timeout", "closed" — stop, report "Triage cancelled." and exit
Do not proceed on any non-submitted status.
Phase 5: Plan Generation
Infer intent from free text
For each issue, classify the operator's instruction:
| Pattern | Action |
|---|
| Empty / blank | Skip — no action taken on this issue |
| Explanatory / conversational (e.g., "this is intentional because...", "already handled in...") | Reply-only — master posts the instruction text as a comment directly, no code change |
| Code-change verbs (e.g., "fix", "refactor", "add", "remove", "update") | Fix task — dispatched via $whip-start |
Ambiguous / unclear (e.g., test, check, hmm, ?, maybe, short placeholders) | Invalid — stop before plan generation and request clarification |
If any instruction is ambiguous or invalid, stop before plan generation and return a compact clarification request:
"Issue #N instruction is ambiguous: ''. Please clarify as one of: fix (code change), reply (comment only), skip (no action)."
Do not guess intent. Resolve all ambiguous instructions before proceeding.
If all issues are skipped, report "No action taken — all issues skipped." and exit.
Handle reply-only items
Reply-only items are handled by the master session directly and bypass $whip-start entirely:
- If
auto_resolve is checked: post the instruction text as a comment on the thread, then resolve the thread
- If
auto_resolve is unchecked: post the instruction text as a comment only
Use the GitHub GraphQL API to comment and resolve:
gh api graphql -f query='
mutation($body: String!, $threadId: ID!) {
addPullRequestReviewThreadReply(input: {body: $body, pullRequestReviewThreadId: $threadId}) {
comment { id }
}
}
' -f body='<comment text>' -f threadId='<thread graphql id>'
gh api graphql -f query='
mutation($threadId: ID!) {
resolveReviewThread(input: {threadId: $threadId}) {
thread { isResolved }
}
}
' -f threadId='<thread graphql id>'
Group fix tasks
Group code-fix issues into tasks:
- Issues on the same file or tightly related files — one task
- Issues on independent files/areas — separate tasks (can run in parallel)
- Never split coupled issues across tasks
Each task carries:
title: descriptive task name
backend: codex for bug fixes and deep research/investigation; agent decides for others
difficulty: based on issue complexity (easy for mechanical, medium for cross-file, hard for subtle bugs)
source_threads: list of issue_key values for traceability
instruction: operator's verbatim instruction(s)
affected_files: file paths from the source threads
auto_resolve: per-issue flag (carried through to completion)
Stale check #1
Before showing the execution preview, re-fetch thread status using the same GraphQL query from Phase 2, filtered to the source thread IDs.
Drop any threads that have been resolved since Phase 2 collection. If a task loses all its source threads, remove that task from the plan. Notify the operator of any dropped items.
Execution preview
Present the plan for confirmation:
## Execution Preview
| Task | Issues | Files | Backend | Difficulty | Action |
|------|--------|-------|---------|------------|--------|
| <title> | #1, #2 | auth.go | codex | medium | fix |
| <title> | #3 | handler.go | — | — | reply-only (done) |
Code fix tasks: N
Reply-only items: M (already handled above)
Skipped: K
Dispatch mode: Solo | Team (auto-selected based on task count)
The operator must confirm before dispatch. If they reject, return to Phase 4 (re-open webform with previous values).
Dispatch mode selection
- 1 fix task — Solo Flow via
$whip-start
- 2+ independent fix tasks — Team Flow via
$whip-start
- Complex interdependent tasks — Lead Flow via
$whip-start (rare for PR followup)
Phase 6: Dispatch
Run $whip-start Step 0 (health check, IRC selection, polling setup).
Task description contract
Each fix task dispatched to $whip-start carries this description:
## Context
PR #<number> (<title>) received review feedback. This task addresses unresolved review thread(s).
Source threads:
- thread-<id>: @<reviewer> on <file>:<line> — "<problem_statement>"
## Objective
<operator's verbatim instruction for the grouped issues>
## Scope
- In: <affected_files>
- Out: everything else
## Implementation Details
- PR branch: <head_branch>
- Review thread URLs: <thread_url list>
- Original reviewer comments and context are provided above in source threads
## Acceptance Criteria
- Code changes address the reviewer's concern(s) as described in the operator's instruction
- Existing tests pass
- Changes are committed to the current branch
Dispatch via $whip-start Solo Flow or Team Flow based on the dispatch mode selected in Phase 5. Use --backend and --difficulty as determined in Phase 5.
Completion: auto-resolve handling
After each task completes successfully, for each source thread in that task where auto_resolve=true:
- Stale check #2: Re-fetch the thread status. If already resolved, skip.
- Comment: Post a comment on the thread summarizing what was done (e.g., "Fixed in commit : "). Use the language that the review conversation is in (e.g., if the reviewer wrote in Korean, reply in Korean). Use 7-character short SHA for commit hashes so GitHub auto-links them correctly.
- Resolve: Resolve the thread.
Use the same GraphQL mutations described in Phase 5 reply-only handling.
For threads where auto_resolve=false, do nothing — the commit is sufficient.
Wrap-up
After all tasks complete:
- Summarize to the operator:
- Total threads triaged
- Fix tasks dispatched and completed
- Reply-only items handled
- Threads auto-resolved
- Threads skipped
- Follow
$whip-start cleanup conventions (stop polling, disconnect IRC)
Edge Cases
- No PR for current branch: Stop with clear message (Phase 1)
- No unresolved threads: Report "all clear" and exit (Phase 2)
- Permission/auth errors for org repos: Respect CLAUDE.md GH_TOKEN override guidance (e.g.,
GH_TOKEN=$GH_TOKEN_SENDBIRD gh api ...)
- Outdated threads: Include in triage but flag as outdated in the webform context
- Comments without line numbers: Show as general comment (
file_path/line displayed as "general comment")
- Duplicate concerns across reviewers: Present separately in the form; operator can group them via similar instruction text
- All issues skipped in triage: Report "no action taken" and exit
- Webform cancelled or timed out: Report "triage cancelled" and exit
- Thread resolved between collection and dispatch (stale): Stale check #1 drops it from the plan; stale check #2 skips auto-resolve
- Ambiguous instruction text: Surface back to operator for clarification before proceeding
- Webform launched but browser did not open: Surface the URL from stderr and continue
- Webform URL emitted but process exited early: Invalidate that URL and re-launch
- Submitted JSON exists but instructions are unclassifiable: Request clarification and stop
- Duplicate threads across reviewers: Preserve separate thread IDs, but allow shared handling through clustering