dyadswarm-pr-review

star 20.6k

Team-based PR review using Claude Code swarm. Spawns three specialized teammates (correctness expert, code health expert, UX wizard) who review the PR diff, discuss findings with each other, and reach consensus on real issues. Posts a summary with merge verdict and inline comments for HIGH/MEDIUM issues.

dyad-sh By dyad-sh schedule Updated 2/11/2026

name: dyad:swarm-pr-review description: Team-based PR review using Claude Code swarm. Spawns three specialized teammates (correctness expert, code health expert, UX wizard) who review the PR diff, discuss findings with each other, and reach consensus on real issues. Posts a summary with merge verdict and inline comments for HIGH/MEDIUM issues.

Swarm PR Review

This skill uses Claude Code's agent team (swarm) functionality to perform a collaborative PR review with three specialized reviewers who discuss and reach consensus.

Overview

  1. Fetch PR diff and existing comments
  2. Create a review team with 3 specialized teammates
  3. Each teammate reviews the diff from their expert perspective
  4. Teammates discuss findings to reach consensus on real issues
  5. Team lead compiles final review with merge verdict
  6. Post summary comment + inline comments to GitHub

Team Members

Name Role Focus
correctness-reviewer Correctness & Debugging Expert Bugs, edge cases, control flow, security, error handling
code-health-reviewer Code Health Expert Dead code, duplication, complexity, meaningful comments, abstractions
ux-reviewer UX Wizard User experience, consistency, accessibility, error states, delight

Workflow

Step 1: Determine PR Number and Repo

Parse the PR number and repo from the user's input. If not provided, try to infer from the current git context:

# Get current repo
gh repo view --json nameWithOwner -q '.nameWithOwner'

# If user provides a PR URL, extract the number
# If user just says "review this PR", check for current branch PR
gh pr view --json number -q '.number'

Step 2: Fetch PR Diff and Context

IMPORTANT: Always save files to the current working directory (e.g. ./pr_diff.patch), never to /tmp/ or other directories outside the repo. In CI, only the repo working directory is accessible.

# Save the diff to current working directory (NOT /tmp/ or $SCRATCHPAD)
gh pr diff <PR_NUMBER> --repo <OWNER/REPO> > ./pr_diff.patch

# Get PR metadata
gh pr view <PR_NUMBER> --repo <OWNER/REPO> --json title,body,files,headRefOid

# Fetch existing comments to avoid duplicates
gh api repos/<OWNER/REPO>/pulls/<PR_NUMBER>/comments --paginate
gh api repos/<OWNER/REPO>/issues/<PR_NUMBER>/comments --paginate

Save the diff content and existing comments for use in the review.

Step 3: Create the Review Team

Use TeamCreate to create the team:

TeamCreate:
  team_name: "pr-review-<PR_NUMBER>"
  description: "Code review for PR #<PR_NUMBER>"

Step 4: Create Review Tasks

Create 4 tasks:

  1. "Review PR for correctness issues" - Assigned to correctness-reviewer
  2. "Review PR for code health issues" - Assigned to code-health-reviewer
  3. "Review PR for UX issues" - Assigned to ux-reviewer
  4. "Discuss and reach consensus on findings" - Blocked by tasks 1-3, no owner (team-wide)

Step 5: Spawn Teammates

Spawn all 3 teammates in parallel using the Task tool with team_name set to the team name. Each teammate should be a general-purpose subagent.

IMPORTANT: Each teammate's prompt must include:

  1. Their role description (from the corresponding file in references/)
  2. The full PR diff content (inline, NOT a file path - teammates cannot read files from the team lead's scratchpad)
  3. The list of existing PR comments (so they can avoid duplicates)
  4. Instructions to send their findings back as a structured message

Teammate Prompt Template

For each teammate, the prompt should follow this structure:

You are the [ROLE NAME] on a PR review team. Read your role description carefully:

<role>
[Contents of references/<role>.md]
</role>

You are reviewing PR #<NUMBER> in <REPO>: "<PR TITLE>"

<pr_description>
[PR body/description]
</pr_description>

Here is the diff to review:

<diff>
[Full diff content]
</diff>

Here are existing PR comments (do NOT flag issues already commented on):

<existing_comments>
[Existing comment data]
</existing_comments>

## Instructions

1. Read your role description carefully and review the diff from your expert perspective.
2. For each issue you find, classify it as HIGH, MEDIUM, or LOW severity using the guidelines in your role description.
3. Send your findings to the team lead using SendMessage with this format:

FINDINGS:
```json
[
  {
    "file": "path/to/file.ts",
    "line_start": 42,
    "line_end": 45,
    "severity": "MEDIUM",
    "category": "category-name",
    "title": "Brief title",
    "description": "Clear description of the issue and its impact",
    "suggestion": "How to fix (optional)"
  }
]
  1. After sending your initial findings, wait for the team lead to share other reviewers' findings.
  2. When you receive other reviewers' findings, discuss them:
    • ENDORSE issues you agree with (even if you missed them)
    • CHALLENGE issues you think are false positives or wrong severity
    • ADD context from your expertise that strengthens or weakens an issue
  3. Send your discussion responses to the team lead.

Be thorough but focused. Only flag real issues, not nitpicks disguised as issues.

IMPORTANT: Cross-reference infrastructure changes (DB migrations, new tables/columns, API endpoints, config entries) against actual usage in the diff. If a migration creates a table but no code in the PR reads from or writes to it, that's dead infrastructure and should be flagged.


### Step 6: Collect Initial Reviews

Wait for all 3 teammates to send their initial findings. Parse the JSON from each teammate's message.

### Step 7: Facilitate Discussion

Once all initial reviews are in:

1. Send each teammate a message with ALL findings from all reviewers (labeled by who found them)
2. Ask them to discuss: endorse, challenge, or add context
3. Wait for discussion responses

The message to each teammate should look like:

All initial reviews are in. Here are the findings from all three reviewers:

Correctness Reviewer Findings:

[list of issues]

Code Health Reviewer Findings:

[list of issues]

UX Reviewer Findings:

[list of issues]

Please review the other reviewers' findings from YOUR expert perspective:

  • ENDORSE issues you agree are real problems (say "ENDORSE: - <reason>")</li> <li>CHALLENGE issues you think are false positives or mis-classified (say "CHALLENGE: <title> - <reason>")</li> <li>If you have additional context that changes the severity, explain why</li> </ul> <p>Focus on issues where your expertise adds value. You don't need to comment on every issue.</p> <pre><code> ### Step 8: Compile Consensus After discussion, compile the final issue list: **Issue Classification Rules:** - An issue is **confirmed** if the original reporter + at least 1 other reviewer endorses it (or nobody challenges it) - An issue is **dropped** if challenged by 2 reviewers with valid reasoning - An issue is **downgraded** if challenged on severity with good reasoning - HIGH/MEDIUM issues get individual inline comments - LOW issues go in a collapsible details section in the summary ### Step 9: Determine Merge Verdict Based on the confirmed issues: - **:white_check_mark: YES - Ready to merge**: No HIGH issues, at most minor MEDIUM issues that are judgment calls - **:thinking: NOT SURE - Potential issues**: Has MEDIUM issues that should probably be addressed, but none are clear blockers - **:no_entry: NO - Do NOT merge**: Has HIGH severity issues or multiple serious MEDIUM issues that NEED to be fixed ### Step 10: Post GitHub Comments #### Summary Comment Post a summary comment on the PR using `gh pr comment`: ```markdown ## :mag: Dyadbot Code Review Summary **Verdict: [VERDICT EMOJI + TEXT]** Reviewed by 3 specialized agents: Correctness Expert, Code Health Expert, UX Wizard. ### Issues Summary | # | Severity | File | Issue | Found By | Endorsed By | |---|----------|------|-------|----------|-------------| | 1 | :red_circle: HIGH | `src/auth.ts:45` | SQL injection in login | Correctness | Code Health | | 2 | :yellow_circle: MEDIUM | `src/ui/modal.tsx:12` | Missing loading state | UX | Correctness | | 3 | :yellow_circle: MEDIUM | `src/utils.ts:89` | Duplicated validation logic | Code Health | - | <details> <summary>:green_circle: Low Priority Notes (X items)</summary> - **Minor naming inconsistency** - `src/helpers.ts:23` (Code Health) - **Could add hover state** - `src/button.tsx:15` (UX) </details> <details> <summary>:no_entry_sign: Dropped Issues (X items)</summary> - **~~Potential race condition~~** - Challenged by Code Health: "State is only accessed synchronously in this context" </details> --- *Generated by Dyadbot code review* </code></pre> <h4>Inline Comments</h4> <p>For each HIGH and MEDIUM issue, post an inline review comment at the relevant line using <code>gh api</code>:</p> <pre><code class="language-bash"># Post a review with inline comments gh api repos/<OWNER/REPO>/pulls/<PR_NUMBER>/reviews \ -X POST \ --input payload.json </code></pre> <p>Where payload.json contains:</p> <pre><code class="language-json">{ "commit_id": "<HEAD_SHA>", "body": "Swarm review: X issue(s) found", "event": "COMMENT", "comments": [ { "path": "src/auth.ts", "line": 45, "body": "**:red_circle: HIGH** | security | Found by: Correctness, Endorsed by: Code Health\n\n**SQL injection in login**\n\nDescription of the issue...\n\n:bulb: **Suggestion:** Use parameterized queries" } ] } </code></pre> <h3>Step 11: Shutdown Team</h3> <p>After posting comments:</p> <ol> <li>Send shutdown requests to all teammates</li> <li>Wait for shutdown confirmations</li> <li>Delete the team with TeamDelete</li> </ol> <h2>Deduplication</h2> <p>Before posting, filter out issues that match existing PR comments:</p> <ul> <li>Same file path</li> <li>Same or nearby line number (within 3 lines)</li> <li>Similar keywords in the issue title appear in the existing comment body</li> </ul> <h2>Error Handling</h2> <ul> <li>If a teammate fails to respond, proceed with the other reviewers' findings</li> <li>If no issues are found by anyone, post a clean summary: ":white_check_mark: No issues found"</li> <li>If discussion reveals all issues are false positives, still post the summary noting the review was clean</li> <li>Always post a summary comment, even if there are no issues</li> <li>Always shut down the team when done, even if there were errors</li> </ul> <h2>File Structure</h2> <pre><code>references/ correctness-reviewer.md - Role description for the correctness expert code-health-reviewer.md - Role description for the code health expert ux-reviewer.md - Role description for the UX wizard </code></pre> </article> </div> <!-- Right: Metadata & Command Sidebar --> <div class="w-full lg:w-80 shrink-0 flex flex-col gap-6" data-astro-cid-7zzsworf> <!-- Install Card --> <div class="p-6 rounded-xl bg-surface-container border border-border/80 flex flex-col gap-4 shadow-sm" data-astro-cid-7zzsworf> <span class="text-xs font-bold uppercase tracking-widest text-on-surface-variant/60 font-mono" data-astro-cid-7zzsworf>Install via CLI</span> <div class="flex flex-col gap-2" data-astro-cid-7zzsworf> <div id="detail-install-cmd" class="font-mono text-[11px] p-3 rounded-lg bg-black/40 border border-border select-all break-all text-primary font-bold leading-relaxed" data-astro-cid-7zzsworf> npx skills add https://github.com/dyad-sh/dyad --skill dyadswarm-pr-review </div> <button id="detail-copy-btn" class="w-full py-2.5 rounded-lg bg-primary hover:bg-primary-hover text-on-primary font-sans font-bold text-sm shadow transition-all active:scale-95 flex items-center justify-center gap-1.5" data-astro-cid-7zzsworf> <span class="material-symbols-outlined text-[16px]" data-astro-cid-7zzsworf>content_copy</span> <span data-astro-cid-7zzsworf>Copy Command</span> </button> </div> </div> <!-- Details & Stats Card --> <div class="p-6 rounded-xl bg-surface-container border border-border/80 flex flex-col gap-4 shadow-sm text-on-surface" data-astro-cid-7zzsworf> <span class="text-xs font-bold uppercase tracking-widest text-on-surface-variant/60 font-sans" data-astro-cid-7zzsworf>Repository Details</span> <div class="flex flex-col gap-3.5" data-astro-cid-7zzsworf> <div class="flex justify-between items-center text-sm" data-astro-cid-7zzsworf> <span class="text-on-surface-variant/70 flex items-center gap-1.5" data-astro-cid-7zzsworf> <span class="material-symbols-outlined text-[16px] text-on-surface-variant/60" data-astro-cid-7zzsworf>star</span> Stars </span> <span class="font-mono font-bold text-on-surface" data-astro-cid-7zzsworf>20,646</span> </div> <div class="flex justify-between items-center text-sm" data-astro-cid-7zzsworf> <span class="text-on-surface-variant/70 flex items-center gap-1.5" data-astro-cid-7zzsworf> <span class="material-symbols-outlined text-[16px] text-on-surface-variant/60" data-astro-cid-7zzsworf>call_split</span> Forks </span> <span class="font-mono font-bold text-on-surface" data-astro-cid-7zzsworf>2,467</span> </div> <div class="flex justify-between items-center text-sm" data-astro-cid-7zzsworf> <span class="text-on-surface-variant/70 flex items-center gap-1.5" data-astro-cid-7zzsworf> <span class="material-symbols-outlined text-[16px] text-on-surface-variant/60" data-astro-cid-7zzsworf>navigation</span> Branch </span> <span class="font-mono bg-surface border border-border px-2 py-0.5 rounded text-[11px] text-on-surface-variant" data-astro-cid-7zzsworf>main</span> </div> <div class="flex justify-between items-start text-sm" data-astro-cid-7zzsworf> <span class="text-on-surface-variant/70 flex items-center gap-1.5 mt-0.5" data-astro-cid-7zzsworf> <span class="material-symbols-outlined text-[16px] text-on-surface-variant/60" data-astro-cid-7zzsworf>article</span> Path </span> <span class="font-mono bg-surface border border-border px-2 py-0.5 rounded text-[11px] text-on-surface-variant truncate max-w-[150px]" title="SKILL.md" data-astro-cid-7zzsworf>SKILL.md</span> </div> </div> </div> <!-- Occupations Tag Card --> <div class="p-6 rounded-xl bg-surface-container border border-border/80 flex flex-col gap-3 shadow-sm" data-astro-cid-7zzsworf> <span class="text-xs font-bold uppercase tracking-widest text-on-surface-variant/60 font-sans" data-astro-cid-7zzsworf>Occupations</span> <div class="flex flex-wrap gap-1.5" data-astro-cid-7zzsworf> <a href="/?occupation=software-developers" class="px-3 py-1 rounded-full bg-primary/10 border border-primary/20 text-primary hover:bg-primary/20 text-[11px] font-semibold transition-all hover:scale-102" data-astro-cid-7zzsworf> Software Developers </a> </div> </div> <!-- Related Creators Card --> <div class="p-6 rounded-xl bg-surface-container border border-border/80 flex flex-col gap-3 shadow-sm" data-astro-cid-7zzsworf> <span class="text-xs font-bold uppercase tracking-widest text-on-surface-variant/60 font-sans" data-astro-cid-7zzsworf>More from Creator</span> <div class="flex items-center gap-2" data-astro-cid-7zzsworf> <img class="w-8 h-8 rounded-full border border-border" src="https://avatars.githubusercontent.com/u/183970190?v=4" alt="dyad-sh" onerror="this.src='https://avatars.githubusercontent.com/u/9919?v=4'" data-astro-cid-7zzsworf> <div class="flex flex-col min-w-0" data-astro-cid-7zzsworf> <span class="font-bold text-sm truncate text-on-surface" data-astro-cid-7zzsworf>dyad-sh</span> <a href="/?creator=dyad-sh" class="text-xs text-primary hover:underline font-semibold transition-all" data-astro-cid-7zzsworf>Explore all skills →</a> </div> </div> </div> </div> </div> </div> </div> <script> const copyBtn = document.getElementById("detail-copy-btn"); const installCmd = document.getElementById("detail-install-cmd"); if (copyBtn && installCmd) { copyBtn.addEventListener("click", () => { const cmd = installCmd.textContent.trim(); navigator.clipboard.writeText(cmd).then(() => { const originalText = copyBtn.innerHTML; copyBtn.innerHTML = ` <span class="material-symbols-outlined text-[16px]">check</span> <span>Copied!</span> `; copyBtn.style.background = "#10b981"; copyBtn.style.borderColor = "#10b981"; setTimeout(() => { copyBtn.innerHTML = originalText; copyBtn.style.background = ""; copyBtn.style.borderColor = ""; }, 1500); }); }); } </script> </div> <!-- Footer --> <footer class="border-t border-border bg-surface-container-low text-on-surface-variant py-8 px-gutter mt-16 rounded-xl"> <div class="max-w-container-max mx-auto flex flex-col md:flex-row justify-between items-center gap-6"> <div class="flex items-center gap-2"> <div class="w-6 h-6 rounded bg-primary bg-opacity-20 flex items-center justify-center"> <span class="material-symbols-outlined text-primary text-sm">code_blocks</span> </div> <span class="font-bold text-on-surface text-sm">SkillMD</span> </div> <div class="flex flex-wrap justify-center gap-6 text-sm"> <a href="/about" class="hover:text-primary transition-colors">About Us</a> <a href="/contact" class="hover:text-primary transition-colors">Contact Us</a> <a href="/privacy" class="hover:text-primary transition-colors">Privacy Policy</a> <a href="/terms" class="hover:text-primary transition-colors">Terms of Service</a> <a href="/support" class="hover:text-primary transition-colors">Support</a> </div> <div class="text-xs text-on-surface-variant/80"> © 2026 SkillMD. All rights reserved. </div> </div> </footer> </main> <!-- Script for Theme Toggle, Mobile Menu, and Sidebar Filter Redirection --> <script> // Theme setup const savedTheme = localStorage.getItem("theme") || "dark"; function applyTheme(theme) { document.documentElement.classList.remove("dark", "green", "dracula", "nord"); if (theme === "dark") { document.documentElement.classList.add("dark"); } else if (theme === "green") { document.documentElement.classList.add("dark", "green"); } else if (theme === "dracula") { document.documentElement.classList.add("dark", "dracula"); } else if (theme === "nord") { document.documentElement.classList.add("dark", "nord"); } document.documentElement.setAttribute("data-theme", theme); const themeMoon = document.getElementById("theme-moon"); const themeSun = document.getElementById("theme-sun"); const themeLeaf = document.getElementById("theme-leaf"); const themeDracula = document.getElementById("theme-dracula"); const themeNord = document.getElementById("theme-nord"); if (themeMoon && themeSun && themeLeaf && themeDracula && themeNord) { themeMoon.style.display = theme === "dark" ? "inline" : "none"; themeSun.style.display = theme === "light" ? "inline" : "none"; themeLeaf.style.display = theme === "green" ? "inline" : "none"; themeDracula.style.display = theme === "dracula" ? "inline" : "none"; themeNord.style.display = theme === "nord" ? "inline" : "none"; } } applyTheme(savedTheme); const themeToggleBtn = document.getElementById("theme-toggle-btn"); if (themeToggleBtn) { themeToggleBtn.addEventListener("click", () => { const currentTheme = document.documentElement.getAttribute("data-theme") || "dark"; let newTheme = "dark"; if (currentTheme === "dark") { newTheme = "light"; } else if (currentTheme === "light") { newTheme = "green"; } else if (currentTheme === "green") { newTheme = "dracula"; } else if (currentTheme === "dracula") { newTheme = "nord"; } else { newTheme = "dark"; } applyTheme(newTheme); localStorage.setItem("theme", newTheme); }); } // Mobile menu toggle and sidebar logic const mobileMenuToggle = document.getElementById("mobile-menu-toggle"); const sidebarMenu = document.getElementById("sidebar-menu"); const sidebarOverlay = document.getElementById("sidebar-overlay"); function isMobile() { return window.innerWidth < 768; // 768px is the 'md' breakpoint in Tailwind } function openSidebar() { if (sidebarMenu) { sidebarMenu.classList.remove("-translate-x-full"); } if (sidebarOverlay) { sidebarOverlay.classList.remove("hidden"); } } function closeSidebar() { if (sidebarMenu && isMobile()) { sidebarMenu.classList.add("-translate-x-full"); } if (sidebarOverlay) { sidebarOverlay.classList.add("hidden"); } } if (mobileMenuToggle && sidebarMenu) { mobileMenuToggle.addEventListener("click", (e) => { e.stopPropagation(); if (isMobile()) { const isClosed = sidebarMenu.classList.contains("-translate-x-full"); if (isClosed) { openSidebar(); } else { closeSidebar(); } } }); document.addEventListener("click", (e) => { if (isMobile()) { if (!sidebarMenu.contains(e.target) && !mobileMenuToggle.contains(e.target)) { closeSidebar(); } } }); if (sidebarOverlay) { sidebarOverlay.addEventListener("click", () => { if (isMobile()) { closeSidebar(); } }); } // Collapse sidebar when clicking a filter button, creator button, or nav item inside it sidebarMenu.addEventListener("click", (e) => { if (isMobile()) { const clickTarget = e.target.closest("button, a"); if (clickTarget) { closeSidebar(); } } }); // Sync sidebar state on window resize window.addEventListener("resize", () => { if (!isMobile()) { // Desktop: sidebar should be visible, no overlay if (sidebarMenu) { sidebarMenu.classList.remove("-translate-x-full"); } if (sidebarOverlay) { sidebarOverlay.classList.add("hidden"); } } else { // Mobile: start collapsed if (sidebarMenu) { sidebarMenu.classList.add("-translate-x-full"); } if (sidebarOverlay) { sidebarOverlay.classList.add("hidden"); } } }); } // If not on homepage, redirect on sidebar filter click const isHomepage = window.location.pathname === "/"; document.querySelectorAll("#occupation-filters .filter-btn").forEach(btn => { btn.addEventListener("click", (e) => { const occ = e.currentTarget.getAttribute("data-occupation"); if (!isHomepage) { window.location.href = occ ? `/?occupation=${encodeURIComponent(occ)}` : "/"; } }); }); document.querySelectorAll("#creator-filters .creator-btn").forEach(btn => { btn.addEventListener("click", (e) => { const creator = e.currentTarget.getAttribute("data-creator"); if (!isHomepage) { window.location.href = `/?creator=${encodeURIComponent(creator)}`; } }); }); </script> </body> </html>