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.
gh auth status. If unauthenticated, stop and tell the human to run gh auth login.gh api user --jq .login
Do NOT assume a login from git config, memory, or any other source.
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}:
{scope} = --owner {org} for all gh search prs calls.{scope} = `` (no scope flag).gh api user/orgs returns an empty list or an error, warn the human and default to asking them to type an org name manually or say “all”.Run both queries in parallel. Collect and merge the results — deduplicate by PR URL.
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.
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:
isResolved: true). A resolved thread has been deliberately closed by a reviewer or the author — it is not actionable.isOutdated: true). These reference code that no longer exists in the diff.isResolved: false AND isOutdated: false.Keep a PR in the “unresponded” list if ANY of the following is true (after thread filtering above):
user.login against {gh_login}).CHANGES_REQUESTED and there is no subsequent comment where user.login == {gh_login} acknowledging it.author.login is NOT {gh_login}.Skip the PR (do not include it) if:
{gh_login}) is newer than all unresolved reviewer activity — they have already responded.APPROVED with no comments or concerns.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.
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:
reviews where state == "APPROVED", using the latest review per reviewer (ignore earlier reviews superseded by a later one from the same person).reviewRequests who haven’t responded yet.statusCheckRollup → passing, failing, pending, or none.mergeable + mergeStateStatus → ready, conflicts, blocked, or unknown.Reason for inclusion: approved |
unresponded-comments |
both. |
CHANGES_REQUESTED (regardless of whether other reviewers have approved), override the default action to ADDRESS and set Reason: both if the PR was also in the approved set. Never default to MERGE for a PR with an active unresolved CHANGES_REQUESTED.Write the triage board to:
/tmp/pr-action-board-{YYYYMMDD-HHMMSS}.md
(Include seconds to avoid collisions on re-invocations within the same minute.)
# 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:
MERGEADDRESSSKIP (note it is a draft)TBD and note what the human should considerAfter 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.
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:
/^MERGE$/i → proceed with merge flow (Phase 5a)/^ADDRESS$/i → proceed with address flow (Phase 5b)/^SKIP$/i → log as skipped, no action taken/^TBD$/i → surface to the human for a decision (see below)TBD and surface to the human; do not guessTBD 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
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:
{owner}/{repo}.--squash (default), --merge, or --rebase.Instructions to:
a. Pre-merge check: Run gh pr view {number} --repo {owner}/{repo} --json mergeable,mergeStateStatus,statusCheckRollup,baseRefName and verify:
mergeable is "MERGEABLE" and mergeStateStatus is "CLEAN". If mergeable is null (GitHub is still computing), wait 10 seconds and re-poll up to 3 times. Only treat as a blocker if mergeable is explicitly "CONFLICTING" or mergeStateStatus is a blocking state after all retries.statusCheckRollup has no failures).baseRefName — this is the target branch for post-merge CI monitoring.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:
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.
Fetch available transitions:
Use the Atlassian MCP getTransitionsForJiraIssue tool with the extracted ticket key.
jira_transition: skipped_no_matching_state with the available transition names listed in notes.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.
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).
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:
{owner}/{repo}, and headRefName (the PR’s source branch).{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.status: no_local_clone and do not proceed — the human must check out the repo manually./address-pr-comments for this PR and return its findings before making any code changes or committing anything./build or /critique until the parent skill (PR Action Board) presents the findings to the human and receives approval.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:
questions_for_human explicitly and wait for answers/build to verify the changes compile and tests pass/critique to commit, push, and open/update the PRIf the human answers questions for a PR, pass the answers into the follow-up message verbatim.
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.
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
Update the Summary Table at the top — add an Outcome column with the final status per PR.
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.
/tmp file is local. Do not upload it to Slack, Jira, Confluence, or anywhere else._Posted by Farty Bobo on behalf of @{gh_login}._ — this is non-negotiable per CLAUDE.md.