Help us improve
Share bugs, ideas, or general feedback.
From farty-bobo
Find your open PRs that are either approved or have new unresolved/unresponded comments, compile them into a triage board with human-annotatable MERGE/ADDRESS/REPLY/SKIP actions, then dispatch dedicated agents to execute each decision. Use when the user asks "what PRs do I need to deal with?", "triage my open PRs", "which PRs need my attention?", or similar.
npx claudepluginhub fartybobo/farty-bobo --plugin farty-boboHow this skill is triggered — by the user, by Claude, or both
Slash command
/farty-bobo:pr-action-boardThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Surface every PR that needs your attention in one annotated triage file:
Creates p5.js generative art with seeded randomness, noise fields, and interactive parameter exploration. Use for algorithmic art, flow fields, or particle systems.
Share bugs, ideas, or general feedback.
Surface every PR that needs your attention in one annotated triage file:
The full workflow is a single human approval gate: the skill builds the file with pre-filled actions and pre-drafted proposals, you review and edit, say "done", and all agents fire simultaneously.
All deterministic state checks in this skill are performed by helper scripts co-located with the skill. Before Phase 2, locate the script directory:
SCRIPT_DIR=$(find ~ -maxdepth 7 \
-path "*/farty-bobo/skills/pr-action-board/scripts" \
-type d 2>/dev/null | head -1)
if [[ -z "$SCRIPT_DIR" ]]; then
echo "ERROR: farty-bobo scripts not found. Clone the farty-bobo repo first." >&2
exit 1
fi
Cache $SCRIPT_DIR for use throughout all phases.
Scripts available (all executable, all take stdin/stdout, all output JSON):
| Script | Purpose |
|---|---|
get-teams.sh <org> | Returns [{slug, name, mention}] for the user's teams in one org |
find-mention-prs.sh <login> <org_or_empty> <teams_json> | Open PRs (not mine) where I or my teams are mentioned and I haven't responded |
find-thread-reply-prs.sh <login> <org_or_empty> | Open PRs (not mine) where I commented and got unresponded replies |
check-pr-threads.sh <login> <owner> <repo> <pr_number> | Per-PR check: unresolved review threads + issue comment chains with unresponded replies |
Run gh auth status. If unauthenticated, stop and tell the human to run gh auth login.
Resolve the GitHub login once and cache it:
gh api user --jq .login
Do NOT assume a login from git config, memory, or any other source.
Prompt the human to choose the org scope. Fetch the list of orgs:
gh api user/orgs --jq '.[].login'
Present the list and ask the human to pick one, or "all" to scan across all orgs:
Which org should I scan?
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 gh search prs calls; org name alone for scripts.{scope} = `` (no flag); pass empty string to scripts.Run phases 2a, 2b, and 2c in parallel. Collect and deduplicate by URL.
gh search prs \
--author="@me" \
--state=open \
--review=approved \
--json number,title,url,repository,createdAt,updatedAt,isDraft,labels \
--limit 100 \
{scope}
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}
For each PR, run in parallel (batch up to 10 at a time):
# PR-level conversation comments
gh api repos/{owner}/{repo}/issues/{number}/comments \
--jq '[.[] | {login: .user.login, created_at: .created_at, body: .body}] | sort_by(.created_at)'
# 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)'
# Inline review threads WITH resolution status (GraphQL)
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. A comment or review is "from the human" if and only if author.login / user.login equals {gh_login}.
Inline thread filtering:
isResolved: true and isOutdated: true threads.isResolved: false AND isOutdated: false.Keep a PR in the unresponded list if ANY of:
CHANGES_REQUESTED with no subsequent human acknowledgment.author.login is NOT {gh_login}.Skip if:
APPROVED with no comments.For each ADDRESS PR — generate proposals inline (no agent spawn needed; use the comment data already fetched):
For each unresolved comment or inline thread, synthesize a brief proposed action:
Proposed: [one-sentence description of the change to make]Proposed reply: [draft response based on the comment context and what you know about the PR]CHANGES_REQUESTED review with body → treat as a code change requestThese proposals populate the #### Proposed Changes section in Phase 3. Also identify any open questions the human needs to answer (things the reviewer asked that you can't resolve from context alone) — these go into #### Questions.
Run 2c-i first (TEAMS_JSON is needed by 2c-ii). Once 2c-i completes, run 2c-ii and 2c-iii in parallel — 2c-iii has no dependency on TEAMS_JSON.
TEAMS_JSON=$("$SCRIPT_DIR/get-teams.sh" "{org}")
# If scope is "all orgs", call get-teams.sh for each org individually and merge results.
# If get-teams.sh errors or returns [], proceed with TEAMS_JSON="[]".
MENTION_PRS=$("$SCRIPT_DIR/find-mention-prs.sh" \
"{gh_login}" "{org_or_empty}" "$TEAMS_JSON")
THREAD_REPLY_PRS=$("$SCRIPT_DIR/find-thread-reply-prs.sh" \
"{gh_login}" "{org_or_empty}")
Combine MENTION_PRS and THREAD_REPLY_PRS. Deduplicate by URL. If a PR appears with multiple reasons, set reason to "multiple" and list all reasons in a reasons array.
Cap at 30 PRs from Phase 2c (most recently updated first). Warn and list dropped PR numbers if more are found.
For each REPLY PR — draft replies inline (use the thread/mention context already fetched):
For each unresponded thread or mention, write a draft reply appropriate to the context:
These drafts populate the #### Draft Replies section in Phase 3.
For every unique PR from 2a, 2b, and 2c (up to 50 total — if more, keep the 50 most recently updated, warn and list dropped numbers):
gh pr view {number} --repo {owner}/{repo} \
--json mergeable,mergeStateStatus,statusCheckRollup,reviews,reviewRequests,headRefName,baseRefName
Derive per PR:
reviews where state == "APPROVED", latest review per reviewer.reviewRequests who haven't responded.passing | failing | pending | none.ready | conflicts | blocked | unknown.my-pr (2a/2b) | others-pr (2c).approved | unresponded-comments | both | thread-reply | mention | direct-mention | team-mention | multiple.CHANGES_REQUESTED, default action is ADDRESS regardless of approvals.Write to:
/tmp/pr-action-board-{YYYYMMDD-HHMMSS}.md
The file is the single human approval gate for the entire run. It includes pre-filled actions and pre-drafted proposals for every PR. The human reviews all of it once, edits whatever they want, and says "done" — after which all agents fire in parallel.
# PR Action Board — {YYYY-MM-DD HH:MM:SS}
Scoped to: {org name, or "all orgs"}
GitHub login: @{login}
> Review all Action blocks and proposed items below, then tell me "done" to execute everything.
>
> Top-level actions: MERGE | ADDRESS | REPLY | SKIP
> Per-item decisions: APPROVE | SKIP | EDIT: <your direction or replacement text>
> Questions: Fill in ANSWER: <your response>
---
## My Open PRs
| PR | Title | Repo | Reason | Approvers | CI | Merge Ready | Unresponded | Action |
|----|-------|------|--------|-----------|----|----------- |-------------|--------|
| [#123](url) | Fix login redirect | embarkvet/foo | approved | @alice, @bob | passing | ready | 0 | MERGE |
| [#118](url) | Add PostHog tracking | embarkvet/bar | unresponded-comments | — | failing | blocked | 3 | ADDRESS |
**Total:** N **Approved + ready:** M **Need attention:** K
---
## Others' PRs — Action Needed From Me
| PR | Author | Repo | Reason | Context | Action |
|----|--------|------|--------|---------|--------|
| [#77](url) | @alice | embarkvet/bar | thread-reply | Reply to my comment on `src/auth.ts:14` | REPLY |
| [#55](url) | @bob | embarkvet/baz | direct-mention | @kinanf tagged in comment by @carol | REPLY |
**Total:** N
---
## PR Details & Actions
<!-- ═══════════════════════════════════════════════════════════════════════ -->
### [My PR] [#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
<!-- MERGE | ADDRESS | SKIP -->
<!-- ═══════════════════════════════════════════════════════════════════════ -->
### [My PR] [#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
#### Proposed Changes
<!-- For each item: APPROVE to accept as-is, SKIP to ignore, or EDIT: <direction> to override. -->
**[C1]** @carol · 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."
Proposed: Memoize the analytics call with `useMemo` at `src/tracking.ts:28`.
APPROVE
**[C2]** @dave · inline · `src/tracking.ts:42`
> "Why not use the existing `useAnalytics` hook here instead?"
Proposed reply: "The `useAnalytics` hook doesn't support batched events yet — this is a deliberate short-term workaround."
APPROVE
**[C3]** @carol · PR comment · 12h ago
> "Any update on the memoization fix?"
Proposed reply: "Working on it — will push the fix shortly."
APPROVE
#### Questions
<!-- Fill in each ANSWER field. Leave blank to skip. -->
**[Q1]** Carol mentioned a hardcoded API key in `src/tracking.ts:15`. Address it in this PR or defer?
ANSWER:
### Action
ADDRESS
<!-- MERGE | ADDRESS | SKIP -->
<!-- ═══════════════════════════════════════════════════════════════════════ -->
### [Others' PR] [#77] Refactor auth service — embarkvet/bar
**URL:** https://github.com/embarkvet/bar/pull/77
**Author:** @alice
**Reason:** thread-reply
**Updated:** 3h ago
#### Draft Replies
<!-- For each draft: APPROVE to post as-is, SKIP to not post, or EDIT: <replacement text> to override. -->
**[R1]** Review thread · @alice · `src/auth.ts:14` · 3h ago
> "Do you think we should extract this into a shared helper? Would love your take since you built the original."
Draft: "Yeah, extracting makes sense here. The original auth pattern lives in `src/auth/base.ts` — a shared helper there would stay consistent with how we've structured things. Happy to do a follow-up PR for it if that works for you."
APPROVE
### Action
REPLY
<!-- REPLY | SKIP -->
<!-- ═══════════════════════════════════════════════════════════════════════ -->
### [Others' PR] [#55] Add rate limiting — embarkvet/baz
**URL:** https://github.com/embarkvet/baz/pull/55
**Author:** @bob
**Reason:** direct-mention
**Mentioned at:** 2026-05-07 14:32 UTC
**Updated:** 1d ago
#### Draft Replies
**[R1]** PR comment · @carol · 1d ago
> "@kinanf — can you review the token bucket implementation here? You wrote the original spec."
Draft: "Taking a look — the token bucket logic looks right to me at a glance. One thing worth checking: the refill rate calculation on line 42 assumes wall-clock seconds but the original spec used monotonic time to handle clock skew. Worth verifying that still holds."
APPROVE
### Action
REPLY
<!-- ═══════════════════════════════════════════════════════════════════════ -->
Summary table: include an Action column in both tables pre-populated with the suggested action — makes it easy to scan and change one without opening the details section.
Default actions:
MERGEADDRESSSKIP (note it is a draft)REPLYTBDProposed items:
APPROVEAPPROVECHANGES_REQUESTED reviews: always generate at least one [C] itemEDIT semantics:
EDIT: <direction> on a code-change item → the ADDRESS agent treats your direction as the implementation instruction instead of the original proposal.EDIT: <replacement text> on a reply item → the REPLY agent posts your replacement text verbatim (after prepending the Farty Bobo disclosure).After writing the file, tell the human:
Triage board written to: /tmp/pr-action-board-{timestamp}.md
Open the file and review:
— Top-level Action (MERGE/ADDRESS/REPLY/SKIP) for each PR
— Proposed Changes [C1, C2…]: APPROVE, SKIP, or EDIT: <direction>
— Draft Replies [R1, R2…]: APPROVE, SKIP, or EDIT: <your text>
— Questions [Q1, Q2…]: fill in ANSWER: <response>
Save and tell me "done" — everything will execute in parallel.
Do NOT proceed until the human explicitly says they are done reviewing.
Re-read the triage file. For each PR, extract:
Find the first non-comment, non-blank line inside the ### Action code block:
/^MERGE$/i → Phase 5a/^ADDRESS$/i → Phase 5b/^REPLY$/i → Phase 5c/^SKIP$/i → log as skipped/^TBD$/i → surface to the human before dispatching (see below)TBDFor each **[C\d+]** block in #### Proposed Changes, extract the code block immediately following it:
APPROVE → include this item in the approved changes list with the original proposed actionSKIP → exclude from the approved changes listEDIT: <text> → include with direction set to the text after EDIT:For each **[Q\d+]** block in #### Questions, extract the ANSWER field:
ANSWER: <text> → include in the question-answers mapFor each **[R\d+]** block in #### Draft Replies, extract the code block immediately following it:
APPROVE → use the draft text verbatimSKIP → do not post this replyEDIT: <text> → use the text after EDIT: as the reply bodyFor each PR, build a self-contained payload that will be passed to the executing agent:
// ADDRESS PR example
{
"pr": 118,
"repo": "embarkvet/bar",
"headRefName": "feature/posthog",
"approved_changes": [
{ "id": "C1", "type": "code_change", "direction": "Memoize the analytics call with useMemo at src/tracking.ts:28" },
{ "id": "C2", "type": "discussion_reply", "reply_text": "The useAnalytics hook doesn't support batched events yet — this is a deliberate short-term workaround." },
{ "id": "C3", "type": "discussion_reply", "reply_text": "Working on it — will push the fix shortly." }
],
"question_answers": [
{ "id": "Q1", "answer": "Defer to a separate ticket" }
]
}
// REPLY PR example
{
"pr": 77,
"repo": "embarkvet/bar",
"approved_replies": [
{
"id": "R1",
"target_type": "review_thread",
"first_comment_rest_id": 12345678,
"reply_text": "Yeah, extracting makes sense here..."
}
]
}
Dispatch all PRs with clear MERGE/ADDRESS/REPLY/SKIP actions immediately (Phase 5). For TBD PRs, surface them to the human now, wait for their decision, then add those agents to the parallel batch. Do not hold up the rest of the queue.
Tally: {M} MERGE, {A} ADDRESS, {R} REPLY, {S} SKIP, {T} TBD
Run ALL agents — MERGE, ADDRESS, and REPLY — in a single parallel batch. Send all agent tool calls in one message. Each agent is self-contained and does not require human interaction; all approvals were captured in the triage file.
Name each agent 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. If the list is exhausted, continue with other historical American outlaws.
Each merge agent receives:
PR URL, number, and repo {owner}/{repo}.
Merge strategy — ask the human once before dispatching if not already specified: --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, wait 10 seconds and re-poll up to 3 times.baseRefName for post-merge monitoring.b. Merge:
gh pr merge {number} --repo {owner}/{repo} --{strategy} --delete-branch
If branchProtectionRules returns empty or errors, attempt direct merge. If blocked by required status checks, retry with --auto.
c. Post-merge CI watch: Monitor {baseRefName} for up to 10 minutes, polling every 60 seconds:
gh run list --branch {baseRefName} --repo {owner}/{repo} --limit 3 \
--json databaseId,status,conclusion,name,createdAt
If any run fails, invoke /resolve-ci-failures on {baseRefName}. If not terminal after 10 minutes, return ci_outcome: timed_out — do NOT invoke /resolve-ci-failures for timed-out runs.
d. Jira ticket transition (only after CI is green):
headRefName using [A-Z]+-\d+. If none found, record jira_transition: skipped_no_ticket.getTransitionsForJiraIssue.getJiraIssue — skip if already in or past target state.transitionJiraIssue. On error, record jira_transition: failed and do not block.jira_transition: skipped_ci_not_green.e. Return:
{
"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": "..."
}
Each ADDRESS agent receives the execution payload from Phase 4d (approved_changes list, question_answers) plus:
{owner}/{repo}, headRefNameThe agent must NOT re-do the analysis or ask the human any questions. All decisions are already in the payload.
Instructions:
find ~ -name ".git" -maxdepth 5 ...), check out {headRefName} (git checkout {headRefName} && git pull). If no local clone is found, return status: no_local_clone.approved_changes:
type: "code_change" → implement the change described in direction. Use the original comment for context.type: "discussion_reply" → post the reply_text to the appropriate comment thread on GitHub. Prepend the Farty Bobo disclosure.[Q] items (not in question_answers): note them in the return payload as unanswered_questions — do not block execution./build to verify compilation and tests pass./critique to commit, push, and open/update the PR.{
"pr": 118, "repo": "owner/repo",
"status": "addressed | partial | error",
"changes_applied": ["C1", "C2"],
"changes_skipped": [],
"replies_posted": ["C2", "C3"],
"unanswered_questions": ["Q1"],
"notes": "..."
}
Each REPLY agent receives the execution payload from Phase 4d (approved_replies list) plus:
{owner}/{repo}, PR authorThe agent must NOT re-draft or ask for approval. All reply text is already in the payload.
Instructions:
approved_replies:
_Posted by Farty Bobo on behalf of @{gh_login}._\n\n to the reply text.target_type: "review_thread" → post via:
gh api -X POST repos/{owner}/{repo}/pulls/{number}/comments/{first_comment_rest_id}/replies \
-f body="..."
where first_comment_rest_id is the REST integer comment ID (not the GraphQL node ID) from the payload.target_type: "pr_comment" → post via:
gh api -X POST repos/{owner}/{repo}/issues/{number}/comments \
-f body="..."
{
"pr": 77, "repo": "owner/repo",
"status": "replied | partial | error",
"replies_posted": ["R1"],
"replies_skipped": [],
"notes": "..."
}
Phase 6 runs after ALL agents have returned. The parent skill writes the triage file; sub-agents do not.
Re-read the triage file.
Append #### Outcome under each PR's ### Action block:
#### Outcome
**Status:** merged | addressed | replied | skipped | blocked
**Completed:** {timestamp}
**Details:** {one-sentence summary}
**CI post-merge:** passing | failing | timed_out | n/a
**Jira:** {ticket} → Done | skipped ({reason}) | n/a
Update both summary tables to add an Outcome column.
Report to the human:
PR Action Board — complete.
✓ MERGE [#123] embarkvet/foo — merged. CI passing. BBH-1915 → Done.
✓ ADDRESS [#118] embarkvet/bar — 3 changes applied, 2 replies posted, critique passed.
✓ REPLY [#77] embarkvet/bar — 1 thread reply posted.
✓ REPLY [#55] embarkvet/baz — 1 mention reply posted.
— SKIP [#101] embarkvet/qux — skipped per your instruction.
✗ MERGE [#109] embarkvet/qux — blocked: merge conflicts. Needs manual rebase.
Updated triage file: /tmp/pr-action-board-{timestamp}.md
Surface any failures or blockers with suggested next steps.
_Posted by Farty Bobo on behalf of @{gh_login}._/tmp file is local only.