PR Action Board Skill

Surface every open PR you authored that needs your attention — approved and waiting to merge, or sitting on unanswered reviewer comments — and let you decide what to do about each one from a single annotated file.


Phase 1 — Preflight

  1. Run gh auth status. If unauthenticated, stop and tell the human to run gh auth login.
  2. Resolve the GitHub login once and cache it for the session:
    gh api user --jq .login
    

    Do NOT assume a login from git config, memory, or any other source.

  3. Prompt the human to choose the org scope. Fetch the list of orgs the authenticated user belongs to:
    gh api user/orgs --jq '.[].login'
    

    Present the list to the human and ask them to pick one, or to type “all” to scan across all orgs with no restriction:

    Which org should I scan for your PRs?
    
      1) embarkvet
      2) acme-corp
      3) all orgs (no restriction)
    
    Enter a number or org name:
    

    Do NOT proceed until the human responds. Cache their choice as {scope}:


Phase 2 — Scan for Actionable PRs

Run both queries in parallel. Collect and merge the results — deduplicate by PR URL.

2a. Approved PRs

gh search prs \
  --author="@me" \
  --state=open \
  --review=approved \
  --json number,title,url,repository,createdAt,updatedAt,isDraft,labels \
  --limit 100 \
  {scope}   # --owner {org} or empty — set in Phase 1

Exclude drafts (isDraft: true) unless the human explicitly asked to include them.

2b. PRs with new unresponded comments

gh search prs \
  --author="@me" \
  --state=open \
  --json number,title,url,repository,createdAt,updatedAt,isDraft \
  --limit 100 \
  {scope}   # --owner {org} or empty — set in Phase 1

For each PR returned, run the following in parallel (batch up to 10 at a time to avoid rate limits):

# Get all PR-level conversation comments (issue-style — no resolution concept here)
gh api repos/{owner}/{repo}/issues/{number}/comments \
  --jq '[.[] | {login: .user.login, created_at: .created_at, body: .body}] | sort_by(.created_at)'

# Get formal review states
gh api repos/{owner}/{repo}/pulls/{number}/reviews \
  --jq '[.[] | {login: .user.login, state: .state, submitted_at: .submitted_at, body: .body}] | sort_by(.submitted_at)'

# Get inline review threads WITH resolution status (GraphQL — REST does not expose isResolved)
gh api graphql -f query='
  query($owner: String!, $repo: String!, $number: Int!) {
    repository(owner: $owner, name: $repo) {
      pullRequest(number: $number) {
        reviewThreads(first: 100) {
          nodes {
            isResolved
            isOutdated
            comments(first: 20) {
              nodes {
                author { login }
                createdAt
                body
                path
                line
              }
            }
          }
        }
      }
    }
  }
' -f owner="{owner}" -f repo="{repo}" -F number={number}

Identity anchor: Use the cached {gh_login} from Phase 1 as the authoritative identity for all comment filtering. A comment or review is “from the human” if and only if its author.login (GraphQL) or user.login (REST) equals {gh_login}. Do not infer identity from PR author, assignee, or any other field.

Inline thread filtering rules — apply before counting or surfacing anything:

Keep a PR in the “unresponded” list if ANY of the following is true (after thread filtering above):

Skip the PR (do not include it) if:

Unresponded comment count (the number shown in the triage table and PR detail section) must only include unresolved, non-outdated threads and unacknowledged conversation comments. Never count resolved or outdated threads toward this number.

2c. Enrich each PR

For every unique PR collected from 2a and 2b (up to 50 total after deduplication — if more, keep the 50 most recently updated by updatedAt descending, warn the human, and list the dropped PR numbers so they know they were skipped), fetch enrichment:

gh pr view {number} --repo {owner}/{repo} \
  --json mergeable,mergeStateStatus,statusCheckRollup,reviews,reviewRequests,headRefName,baseRefName

Derive per PR:


Phase 3 — Build the Triage Board File

Write the triage board to:

/tmp/pr-action-board-{YYYYMMDD-HHMMSS}.md

(Include seconds to avoid collisions on re-invocations within the same minute.)

File format

# PR Action Board — {YYYY-MM-DD HH:MM:SS}

Scoped to: {org name, or "all orgs"}
GitHub login: @{login}

---

## Summary Table

| PR | Title | Repo | Reason | Approvers | CI | Merge Ready | Unresponded | Updated |
|----|-------|------|--------|-----------|----|-----------  |-------------|---------|
| [#123](url) | Fix login redirect | embarkvet/foo | approved | @alice, @bob | passing | ready | 0 | 2h ago |
| [#118](url) | Add PostHog tracking | embarkvet/bar | unresponded-comments | — | failing | blocked | 3 | 1d ago |

**Total PRs:** N  
**Approved + ready to merge:** M  
**Blocked / need attention:** K

---

## PR Details & Actions

<!-- ═══════════════════════════════════════════════════════════ -->

### [#123] Fix login redirect — embarkvet/foo

**URL:** https://github.com/embarkvet/foo/pull/123  
**Branch:** `fix/login-redirect``main`  
**Reason:** approved  
**Approvers:** @alice, @bob  
**Pending reviewers:** none  
**CI:** passing  
**Merge ready:** ready  
**Unresponded comments:** 0  

#### Reviewer Activity

*(none — approved cleanly)*

### Action

MERGE


<!-- Set to one of: MERGE | ADDRESS | SKIP -->
<!-- MERGE  → merge this PR, then monitor CI with /resolve-ci-failures -->
<!-- ADDRESS → address unresolved comments with /address-pr-comments -->
<!-- SKIP   → do nothing this round -->

<!-- ═══════════════════════════════════════════════════════════ -->

### [#118] Add PostHog tracking — embarkvet/bar

**URL:** https://github.com/embarkvet/bar/pull/118  
**Branch:** `feature/posthog` → `main`  
**Reason:** unresponded-comments  
**Approvers:** none  
**Pending reviewers:** @carol  
**CI:** failing  
**Merge ready:** blocked  
**Unresponded comments:** 3  

#### Reviewer Activity

- @carol (2d ago, CHANGES_REQUESTED): "This will fire an event on every render — should be memoized. Also the API key is hardcoded, that needs to be an env var."
- @dave (1d ago, inline on `src/tracking.ts:42`): "Why not use the existing `useAnalytics` hook here instead?"
- @carol (12h ago, PR comment): "Any update on the memoization fix?"

### Action

ADDRESS


<!-- Set to one of: MERGE | ADDRESS | SKIP -->

Include the #### Reviewer Activity section only for PRs with unresponded comments — list each unresponded comment/review in chronological order, truncated to 200 chars. For approved PRs with no unresponded comments, write *(none — approved cleanly)*.

Default action values:

After writing the file, tell the human:

Triage board written to: /tmp/pr-action-board-{timestamp}.md

Open the file and set the Action for each PR:
  MERGE    → I will merge it and monitor CI (default for approved + clean)
  ADDRESS  → I will spin up an agent to address reviewer comments
  SKIP     → skip this round

Save the file and tell me "done" to execute.

Do NOT proceed until the human explicitly says they are done annotating.


Phase 4 — Parse Annotations

Re-read the triage file. For each PR’s ### Action section, find the first non-comment, non-blank line inside the code block that matches one of:

TBD handling: Dispatch all PRs with clear MERGE/ADDRESS/SKIP annotations immediately. Surface TBD and unrecognized PRs to the human in parallel, and dispatch their agents as soon as the human resolves each one. Do not block the entire queue waiting for a single TBD.

Tally: {M} MERGE, {A} ADDRESS, {S} SKIP, {T} TBD


Phase 5 — Execute Actions

5a. MERGE flow (one agent per PR, run in parallel)

For each MERGE-annotated PR, spin up a dedicated general-purpose agent named after a unique American outlaw from the 1800s–1900s (e.g. Butch Cassidy, Jesse James, Belle Starr, Black Bart, Dutch Schultz, Pretty Boy Floyd, Billy the Kid, Bonnie Parker, Sam Bass, Pearl Hart, John Wesley Hardin, Cole Younger, Doc Holliday, Calamity Jane, Tom Horn, Kid Curry, Sundance Kid, Cherokee Bill, Cattle Annie, Emmett Dalton). Names must be unique across all agents in this session — including across MERGE and ADDRESS agents. If the named list is exhausted, continue generating unique names from other historical American outlaws of the 1800s–1900s not already used.

Each merge agent receives a self-contained prompt with:

  1. The PR URL and number, and repo {owner}/{repo}.
  2. The merge strategy to use — ask the human once before dispatching if not already specified: --squash (default), --merge, or --rebase.
  3. Instructions to:

    a. Pre-merge check: Run gh pr view {number} --repo {owner}/{repo} --json mergeable,mergeStateStatus,statusCheckRollup,baseRefName and verify:

    b. Merge:

       gh pr merge {number} --repo {owner}/{repo} --{strategy} --delete-branch
    

    For the --auto flag: check gh repo view {owner}/{repo} --json branchProtectionRules. If the field returns an empty array or an error (not all plan tiers expose it), attempt a direct merge without --auto. If the direct merge fails with an error indicating required status checks, retry with --auto and note this in the outcome notes field.

    c. Post-merge CI watch: After the merge, monitor CI on {baseRefName} (from the pre-merge check above — do NOT hardcode main) for up to 10 minutes:

       gh run list --branch {baseRefName} --repo {owner}/{repo} --limit 3 --json databaseId,status,conclusion,name,createdAt
    

    Poll every 60 seconds. If any run fails within the monitoring window, invoke /resolve-ci-failures on {baseRefName} of that repo. If CI has not reached a terminal state after 10 minutes, return ci_outcome: timed_out in the JSON result and note that the run was still in progress at timeout — do NOT invoke /resolve-ci-failures for a timed-out run; surface it to the human for manual follow-up instead.

    d. Jira ticket transition (only after CI is green): Once CI reaches a passing state — either originally passing, or passing after /resolve-ci-failures completes successfully — attempt to transition the associated Jira ticket to Done:

    1. Extract the ticket key from the PR title and branch name using the pattern [A-Z]+-\d+ (e.g. BBH-1915, PROJ-42). Check the PR title first, then headRefName. Use the first match found. If no ticket key is found, skip this step entirely and record jira_transition: skipped_no_ticket in the JSON result.

    2. Fetch available transitions: Use the Atlassian MCP getTransitionsForJiraIssue tool with the extracted ticket key.

    3. Select the target transition using this priority order (case-insensitive substring match):
      • “Done” → first choice
      • “Merged” → second choice
      • “Released” → third choice
      • “Closed” → fourth choice If none match, skip the transition and record jira_transition: skipped_no_matching_state with the available transition names listed in notes.
    4. Idempotency check: Fetch the ticket’s current status via getJiraIssue. If it is already in the matched target state or any downstream state (e.g. already “Done”), skip the transition and record jira_transition: skipped_already_done.

    5. Apply the transition using transitionJiraIssue. If the MCP connector is unavailable or the API returns an error, record jira_transition: failed with the error in notes — do not retry, do not block the merge outcome report.

    This step must be skipped entirely (record jira_transition: skipped_ci_not_green) if CI did not reach a passing state — i.e., if ci_outcome is failing, timed_out, or skipped.

    e. Return a JSON result:

       {
         "pr": 123,
         "repo": "owner/repo",
         "status": "merged | blocked | error",
         "merge_sha": "abc123",
         "ci_outcome": "passing | failing | timed_out | skipped",
         "jira_ticket": "BBH-1915 | null",
         "jira_transition": "done | skipped_no_ticket | skipped_no_matching_state | skipped_already_done | skipped_ci_not_green | failed",
         "notes": "any relevant detail"
       }
    

Run all MERGE agents in parallel (single message, multiple tool calls).

5b. ADDRESS flow (one agent per PR, run sequentially — each requires human interaction)

For each ADDRESS-annotated PR, spawn a dedicated general-purpose agent (use remaining unique outlaw names). Run these sequentially, one at a time — each agent will need to present findings to the human and wait for input before proceeding.

Each address agent receives a self-contained prompt with:

  1. The PR URL, number, repo {owner}/{repo}, and headRefName (the PR’s source branch).
  2. The unresponded reviewer activity captured in Phase 2 (list the comments verbatim so the agent does not have to re-fetch).
  3. Instructions to locate the correct local clone of {owner}/{repo} (via find ~ -name ".git" -maxdepth 5 -type d | xargs -I{} dirname {} | xargs -I{} sh -c 'cd {} && git remote get-url origin 2>/dev/null | grep -q "{repo}" && echo {}' or similar), check it out to {headRefName} (git checkout {headRefName} && git pull), and then invoke /address-pr-comments. The /address-pr-comments skill operates on the current branch of the working directory — it will fail silently or target the wrong PR if the branch is not checked out first.
  4. If no local clone is found, report back to the parent skill with status: no_local_clone and do not proceed — the human must check out the repo manually.
  5. Instructions to invoke /address-pr-comments for this PR and return its findings before making any code changes or committing anything.
  6. Explicit instruction: Do NOT invoke /build or /critique until the parent skill (PR Action Board) presents the findings to the human and receives approval.
  7. The agent must return a structured summary of findings and proposed actions in this format:

    {
      "pr": 118,
      "repo": "owner/repo",
      "proposed_changes": [
        {
          "comment_author": "@carol",
          "comment_summary": "Memoize the event call",
          "proposed_action": "Wrap in useMemo — see src/tracking.ts:42",
          "type": "code_change | discussion | ignore"
        }
      ],
      "discussion_items": [
        {
          "comment_author": "@dave",
          "question": "Why not use useAnalytics hook?",
          "draft_reply": "The useAnalytics hook doesn't support batched events yet — this is a deliberate short-term workaround."
        }
      ],
      "questions_for_human": [
        "Carol also asked about the hardcoded API key — do you want me to move it to env or is there a separate ticket for that?"
      ]
    }
    

After each address agent returns findings:

  1. Present the findings to the human in a clear, structured format:
  2. Wait for the human to respond and confirm which proposed changes to proceed with, which to skip, and how to answer discussion items.
  3. Only after human approval: send a follow-up message to the agent instructing it to:
  4. Wait for the agent to complete before moving to the next ADDRESS PR.

If the human answers questions for a PR, pass the answers into the follow-up message verbatim.


Phase 6 — Update Triage File and Report

Phase 6 runs only after ALL agents — both MERGE and ADDRESS — have returned their results to the parent skill. The parent skill (PR Action Board) performs all writes to the triage file in a single sequential pass. Sub-agents do NOT write to the triage file themselves; they only return structured results.

  1. Re-read the triage file.
  2. For each PR, append an #### Outcome subsection under its ### Action block:

    #### Outcome
    
    **Status:** merged | addressed | skipped | blocked  
    **Completed:** {timestamp}  
    **Details:** {one-sentence summary — e.g. "Merged via squash. CI passed on main." or "3 comments addressed, 1 discussion replied to, PR pushed and critique passed."}  
    **CI post-merge:** passing | failing (see /resolve-ci-failures output below) | timed_out | n/a  
    **Jira:** {ticket key} transitioned to Done | skipped ({reason}) | n/a  
    
  3. Update the Summary Table at the top — add an Outcome column with the final status per PR.

  4. Report to the human in the conversation:

    PR Action Board — complete.
    
    ✓  MERGE  [#123] embarkvet/foo — merged. CI passing. BBH-1915 → Done.
    ✓  MERGE  [#117] embarkvet/foo — merged. CI passing. No ticket found — Jira skipped.
    ✓  ADDRESS [#118] embarkvet/bar — 3 comments addressed, pushed, critique passed.
    —  SKIP   [#101] embarkvet/baz — skipped per your instruction.
    ✗  MERGE  [#109] embarkvet/qux — blocked: merge conflicts. Needs manual rebase.
    
    Updated triage file: /tmp/pr-action-board-{timestamp}.md
    

    For any failures or outstanding blockers, surface them explicitly and suggest next steps.


Guardrails