---
name: gh-queue
description: Use when reviewing, triaging, or managing the incoming GitHub backlog on the Breeze repo — PRs, Discussions, AND Issues. Stateful manager with a shared queue file, contributor profiles, parallel pr-review-toolkit dispatch, GraphQL discussion recipes, comment-style conventions, and session-log bookkeeping. Auto-detects object type from the request; bare invocation triages all three. Triggers on "review PR", "PR queue", "triage PRs/discussions/issues", "review discussions", "check open issues", "what's waiting on me", "gh queue", "repo queue", "review #NNN", or any request about managing the open-PR/discussion/issue backlog.
---

# Breeze GH Queue Manager

## Overview

Long-running manager for the **incoming** GitHub backlog on the Breeze repo (`LanternOps/breeze`) — across three object types: **Pull Requests, Discussions, and Issues** (community + Billy + any agent-authored). State persists across sessions in two co-located files:

- **`queue.md`** — live snapshot, split into `## PRs`, `## Discussions`, `## Issues` sections (each with waiting-on-me / in-flight / recently-closed), plus one rolling session log at the bottom.
- **`contributors.md`** — per-contributor profiles and review heuristics (apply across all three object types — Billy's patterns show up in PRs *and* discussions).

Both files are gitignored (the entire `.claude/skills/` tree is in `.gitignore`), so they're a private scratchpad that travels with the repo on this machine but doesn't leak into commits.

**Outgoing items (mine) are out of scope** except to track own-PRs for merge. This skill manages what needs *my review/action*.

## Object-type selection (auto-detect, all-default)

Pick scope from the request — no explicit flag required:

- "review PR #5" / "PR queue" / "triage PRs" → **PRs only**
- "triage discussions" / "review discussions" / "discussion #968" → **Discussions only**
- "any new issues" / "triage issues" / "issue #710" → **Issues only**
- bare "what's waiting on me" / "/pr" / "triage the backlog" / "gh queue" → **all three**

When ambiguous, default to all three but lead with whatever the user named.

## When to invoke

- "What's waiting on me?" / "What's in the queue?" / "Triage the backlog"
- "Review PR #NNN" / "Triage discussions" / "Any new issues?"
- After a PR merges / a discussion is answered / an issue is resolved — to update `queue.md`
- When a new contributor shows up — to start a profile in `contributors.md`
- At the start of a session if the user asks about repo state

## Workflow

### 1. Re-orient

Read `queue.md` first to load current state. Then list live state for the in-scope type(s):

```bash
# PRs
gh pr list --repo LanternOps/breeze --state open --limit 50 \
  --json number,title,author,isDraft,updatedAt,reviewDecision

# Discussions (no gh subcommand — GraphQL; one query gets last-comment author+date)
gh api graphql -f query='
{ repository(owner:"LanternOps", name:"breeze") {
    discussions(first:50, orderBy:{field:UPDATED_AT, direction:DESC}) {
      totalCount
      nodes { number title category{name} isAnswered closed updatedAt
        author{login} comments(last:1){ totalCount nodes{ author{login} createdAt } } }
} } }'

# Issues
gh issue list --repo LanternOps/breeze --state open --limit 50 \
  --json number,title,author,labels,updatedAt,comments
```

Cross-check against `queue.md`. Add anything new, mark anything closed/merged, flag drift.

**Never quote the bare open-issue count as a backlog number.** As of 2026-08-16 the 248 open issues are ~112 `enhancement` (roadmap you filed for yourself) against ~136 defects; the total moves with how fast you file features, not with how fast you fix bugs. Every open issue now carries a label, so use the views rather than the total:

```bash
# The defect list — the number that should actually trend down
gh issue list --repo LanternOps/breeze --state open --limit 200 \
  --search 'label:bug,tech-debt,ci-red'
# Roadmap — feature work; triage it, don't count it as debt
gh issue list --repo LanternOps/breeze --state open --limit 200 --label enhancement
# Waiting on someone else — sweep for stale ones each round
gh issue list --repo LanternOps/breeze --state open --search 'label:pending-author,needs-info'
```

An unlabeled issue is invisible to all three, so **label on sight** — that's what let 69 issues escape triage entirely before 2026-08-16.

**Repeated `--label` is AND, not OR.** `--label bug --label tech-debt` returns only issues carrying *both*, which for these labels is zero — a silently empty result that reads exactly like "no defects." Use `--search 'label:a,b,c'` (comma = OR) whenever you want any-of semantics, and sanity-check the count against `--state open` before believing a small number.

**The universal staleness trap — "I responded but the queue memory is stale" applies to ALL THREE types.** For every open item, compare the *last-activity author + date* against my last engagement. If someone else spoke after me with a date past my last action, **I'm the blocker** — read the full thread before deciding what's open. This is the single most common failure mode and it is type-agnostic:

- **PRs:** `reviewDecision` resets from `CHANGES_REQUESTED` → `REVIEW_REQUIRED` whenever new commits land, so a PR you bounced that's since been fixed looks identical to one the author hasn't touched. For every "REQUEST CHANGES posted" PR, verify `commits.last.committedDate` vs the bounce date:
  ```bash
  for pr in <bounced PR numbers>; do
    gh pr view $pr --repo LanternOps/breeze --json title,updatedAt,reviewDecision,commits \
      | jq -r '"#\($pr): updated \(.updatedAt) | last commit \(.commits|last.committedDate) — \(.commits|last.messageHeadline)"'
  done
  ```
  If `last commit > bounce date`, it's waiting on **me** → re-review (Tier-1 manual; scope is bounded to the original feedback).
- **Discussions:** the `comments(last:1)` author in the re-orient query. `isAnswered=false` on a Q&A I already answered = I still owe an "accept answer." Do NOT read `isAnswered=null` on non-Q&A categories as "open" — that field is null for Ideas/Show-and-tell by design; use last-comment author instead. (Misread this once and flagged a pile of already-closed discussions as open.)
- **Issues:** last comment author + date. A reporter who replied after my last comment is waiting on me.

**Issues: the staleness sweep is a FULL pass, never an activity window.** Iterate every open issue whose author is not me, regardless of `updatedAt`, and compare the last comment's author+date against my last comment. A round that only looks at items updated "since the last round" misses everything that landed during a gap between rounds — that is exactly how #2987 (08-18), #3876 (08-25), #4000 (08-25) and #3386 (08-27) went unanswered for 18 days after a two-week outage in rounds, while the log claimed "every community issue last-comment = Todd." Two corollaries: (1) **a community issue with ZERO comments is waiting on me** — a last-comment check returns nothing and silently drops it, which is what hid the p1 #4000; (2) **never age a prior round's "not waiting on me" verdict** — re-run the comparison on every item every round, because a reporter can nudge after a sign-off (#3386). The one-liner:

```bash
gh issue list --repo LanternOps/breeze --state open --limit 400 --json number,author --jq '.[] | select(.author.login != "ToddHebebrand") | .number' \
  | while read n; do gh issue view $n --repo LanternOps/breeze --json number,author,comments \
      --jq '"#\(.number) by \(.author.login) last=\(if (.comments|length)>0 then "\(.comments[-1].author.login)@\(.comments[-1].createdAt[:10])" else "NONE (waiting on me)" end)"'; done \
  | grep -v 'last=ToddHebebrand'
```
Anything that prints is 🔴 until the thread is opened and read.

**Drafts — `isDraft=true` ≠ "not on me."** A draft whose *body* asks for early design/approach feedback ("opening for early feedback," "thoughts before I build the rest") is genuinely waiting on me. Read the body of every incoming draft before bucketing it author-blocked (missed once on #976). Respond with a design-level COMMENT review, not an approve/merge gate. **The draft flag also never auto-flips on a comment-thread handoff** — #859 sat 18 days bucketed "author-blocked draft" after the author had addressed every blocker and a reviewer posted "leaving the merge decision to @ToddHebebrand," because the staleness check stopped at `isDraft=true`. The last-activity-author rule OUTRANKS the draft flag: apply it to drafts too, every round — if the last substantive comment hands me the ball (LGTM-pending-maintainer, "no rush" posted *after* fixes landed), the item is 🔴 waiting-on-me regardless of draft status.

**Dependabot — "will auto-rebase" is a recurring queue.md lie.** Dependabot only auto-rebases trivial conflicts, NOT lockfile conflicts. For any "auto-rebase pending" PR, check `commits.last.committedDate` vs the note date; if stale, post `@dependabot rebase` explicitly, then merge **sequentially** (10-15s between admin-merges) so GH's mergeability recompute keeps up.

### 2. Triage round

Bucket every in-scope item into:

- 🔴 **Waiting on me** — needs my review/answer/decision
- 🟡 **Ball in their court** — author/reporter to act; note what we're waiting for
- 🟢 **Closeable** — done, just needs the close action (per close rules in §5)

**Verify against code before responding (load-bearing rule).** When a thread *claims* a gap, bug, regression, or "X is broken" — read the actual code and confirm before you answer. Plausible-sounding claims are wrong often enough that posting on them unverified produces public mistakes. Two real misses came from repeating a claim instead of checking: a "security/patches ignore orgId" gap that was already fixed by #973, and a "filters are a scale regression" claim that was actually the existing page architecture. Grep/read first, then respond. A false-negative grep is also a trap — confirm with the real symbol, not a guessed string (searched `query('orgId')` and got zero hits when the code used `query.orgId`).

#### PR-specific tiering — don't waste subagent budget on green-on-sight

**Tier 1 — fast manual scan (~30s, no subagents).** `gh pr diff <N> | head -150` or `gh pr view <N> --json files,additions,deletions`. Green-on-sight: test-only diffs, dependency bumps, explicit verifiable cleanups, trivial config mirroring an existing pattern, dated-TODO removal (verify date + grep for live consumers).

**Tier 2 — parallel pr-review-toolkit subagents (only the 🟡 pile).** Heuristics: touches auth/RBAC/RLS/tenant isolation; new schema/migration; diff >~200 lines or >5 files; novel subsystem; frontend PR from an AI-assisted contributor (check `Dialog.tsx`/`runAction`/helper reinvention); title makes a hard-to-skim claim. Send one message with multiple `Agent` tool uses, `subagent_type: pr-review-toolkit:code-reviewer`:

```
Review PR #<NUM> on LanternOps/breeze. Fetch the diff with:
  gh pr view <NUM> --repo LanternOps/breeze --json title,body,additions,deletions,files
  gh pr diff <NUM> --repo LanternOps/breeze
Context: <2-3 sentences on what it claims + prior discussion>
Focus on: correctness bugs at file:line; tenant isolation / RLS (every new tenant table needs
policies — see CLAUDE.md); whole-system coherence (does the diff match the body? does it reinvent
something?); for Billy's PRs check globally-disconnected patterns (re-implemented modals, body-sniff
vs helper — see contributors.md); comment style (bold leads, file:line, no tables).
Report under 250 words: must-fix vs nice-to-have, file:line + one-line rationale each. Do NOT post
comments — return findings as text for me to consolidate.
```

Consolidate findings before posting. Group must-fix vs nice. When in doubt, lean Tier 1.

#### Discussions & Issues — mostly manual

No subagent tiering. Read the thread, verify claims against code (§2 rule), bucket, respond. Use a subagent only for genuinely large investigations (e.g. "is this proposed feature already half-built?" — like checking whether a filter engine already exists before greenlighting a filter PR). Issues follow the `github-issues` skill for etiquette/lifecycle; this manager owns the queue/triage.

### 3. Single-item deep review

For one PR or an architectural item, skip dispatch and review directly: `gh pr view`/`gh pr diff` first, check linked issues/discussions for context, verify tenancy changes against the RLS shape table in `CLAUDE.md`, check Go `slog` calls use `err.Error()` (raw err serializes as `{}`), verify migration naming `YYYY-MM-DD-<slug>.sql` + same-day `-a-`/`-b-` ordering.

### 4. Posting

**Style (from `feedback_issue_comment_style` memory):** bold section leads (`**Root cause:**`, `**Fix:**`), file:line refs (`apps/api/src/routes/devices.ts:142`), commit SHAs for prior work, **prose not tables**, trailing status line.

**Escaping (from `feedback_gh_comment_escaping`):** `\n` does NOT expand in bash double-quotes — use a heredoc or a temp file. Nested backticks in inline code spans break the comment; rephrase. For long/structured comments, write the body to a temp file and pass it via `-F` (most robust — avoids all shell escaping):

```bash
# PR / Issue comment
gh pr review <N> --repo LanternOps/breeze --request-changes --body "$(cat <<'EOF'
**Root cause:** ...
EOF
)"
gh issue comment <N> --repo LanternOps/breeze --body "$(cat <<'EOF' ... EOF)"

# Discussion comment — GraphQL mutation, body from a temp file
DID=$(gh api graphql -f query='{repository(owner:"LanternOps",name:"breeze"){discussion(number:<N>){id}}}' --jq '.data.repository.discussion.id')
gh api graphql -f query='mutation($id:ID!,$body:String!){addDiscussionComment(input:{discussionId:$id,body:$body}){comment{url}}}' \
  -f id="$DID" -F body=@/tmp/comment.md --jq '.data.addDiscussionComment.comment.url'
```

**Handles (from `feedback_never_invent_github_handles`):** NEVER @-mention a handle you didn't see verbatim in the thread. Verify with `gh issue view --json author,comments` / the discussion query first.

**Cite docs by PUBLISHED URL, code by repo path.** `file:line` is the right currency for *code* — a reader can find `apps/api/src/config/validate.ts:1198` on GitHub. It is the wrong currency for *documentation*: `apps/docs/src/content/docs/deploy/upgrades.mdx:326` is unusable to the self-hoster who asked. That tree publishes to **https://docs.breezermm.com**, mapping `apps/docs/src/content/docs/<path>.mdx` → `https://docs.breezermm.com/<path>/` (Starlight, `site` set in `apps/docs/astro.config.*`, no `base`). Link the page, not the source file, and **curl each URL for a 200 before posting** — a dead link in a public answer is worse than a repo path. Repo docs *outside* that tree (`docs/operations/*.md`, `docs/registers/*.md`) are not published; name them as repo files and say so. Internal issues we file for ourselves are the exception — a dev fixing `backup.mdx` wants the file path.

**Always verify the post landed** (count comments / re-read last comment). The discussion mutation can succeed while a follow-up verify query errors on a missing-`first`/`last` pagination boundary — that's a query bug, not a post failure; check before re-posting or you'll double-comment (happened on #982; deleted the dup via `deleteDiscussionComment(input:{id:"DC_..."})`).

### 5. Closing — rules differ by type

- **PRs:** `main` is behind the merge queue (since 2026-09-07) — a bare `gh pr merge <N> --repo LanternOps/breeze` enqueues; the queue squashes and lands it serially after the full `CI Success` gate. Never `--admin` (bypasses the queue and rebuilds every entry behind it), per CLAUDE.md "PR Merge Process".
- **Discussions:** I *can* close my own. Comment first, then close with a reason:
  ```bash
  gh api graphql -f query='mutation($id:ID!){closeDiscussion(input:{discussionId:$id,reason:RESOLVED}){discussion{number closed}}}' -f id="$DID"
  ```
  `reason: RESOLVED` for answered/shipped, `OUTDATED` for superseded. Leave open anything with genuine outstanding work even if I wrote "closing as resolved" earlier — verify the work actually shipped (e.g. a discussion whose prerequisite PR merged but whose feature PR never did → stays open).
- **Issues:** defer to the `github-issues` skill, which splits the rule by who filed it. **NEVER close community issues yourself** — reporter verifies and closes, or owner closes after reporter confirms, or owner closes stale ones with a note. A commit/deploy is not a verification. **Internally-filed issues are the opposite**: close them once their fix is merged, since no third party is waiting to verify.

**Fixed-but-open sweep.** Because PRs historically used `Refs #N` rather than `Closes #N`, merged fixes do not close their issue. When the open count looks wrong, run the sweep rather than reading titles:

```bash
gh issue list --state open --limit 400 --json number --jq '.[].number' > /tmp/open.txt
git log origin/main --since=<date> --pretty=format:'%H%x09%s%x0a%b%x00' > /tmp/log.txt
# match each open number against every commit subject+body; a subject of the form
# `fix(scope): summary (#ISSUE) (#PR)` is the repo's fully-fixed signature
```

Then read the commit **body** before closing, not the subject: several deliberately say "this is half of #N" or "leaving open for the reporter". Close only the ones with no such caveat, and comment with the fixing PR + SHA.

### 6. After every action

Update `queue.md`:
- Move closed/merged items out of the waiting section of the right type (`## PRs` / `## Discussions` / `## Issues`)
- Add a one-line entry to the session log at the bottom with today's **absolute** date (`### 2026-05-30 — ...`), never "today"/"yesterday"
- If you learned a contributor pattern, update `contributors.md`

## Adding a new contributor

When an item arrives from someone not in `contributors.md`: skim their first few PRs/issues (`gh search prs --author <handle> --repo LanternOps/breeze`), and after 2-3 interactions add a 4-6 line profile (real name + handle, AI-assisted/human/mixed, recurring patterns at file:line, response style). The point is "what to check first when their next item lands." Profiles apply across PRs, discussions, and issues.

## Common gh commands

```bash
# PRs waiting on me / drafts / single PR
gh pr list --repo LanternOps/breeze --state open --search "review-requested:@me" --limit 20
gh pr list --repo LanternOps/breeze --state open --draft --limit 20
gh pr view <N> --repo LanternOps/breeze --comments

# Discussions (all via GraphQL — see §1 list query and §4/§5 mutations)
gh api graphql -f query='{repository(owner:"LanternOps",name:"breeze"){discussion(number:<N>){id title body closed comments(first:30){nodes{author{login} createdAt body}}}}}'

# Issues
gh issue list --repo LanternOps/breeze --state open --limit 50
gh issue view <N> --repo LanternOps/breeze --comments --json title,body,comments,author,labels

# Author history
gh search prs --author <handle> --repo LanternOps/breeze --limit 20

# Merge (enqueue — the merge queue owns the squash; never --admin)
gh pr merge <N> --repo LanternOps/breeze
```

## Defers to (not duplicated here)

- **`pr-review-toolkit:code-reviewer`** — deep PR code review (Tier 2 dispatch)
- **`github-issues`** — issue etiquette, lifecycle, issue-body templates, the never-close-community-issues rule

## Related memories

- `feedback_issue_comment_style` — bold leads, file:line refs, prose not tables
- `feedback_gh_comment_escaping` — heredoc/temp-file for newlines, no nested backticks
- `feedback_never_invent_github_handles` — only @-mention what's verbatim in the thread
- `feedback_verify_own_closes_keywords` — `Closes #X #Y` only auto-closes #X; grep the merged diff per claimed item

## Out of scope

- Outgoing PRs (mine) beyond merge-tracking — use TodoWrite per session
- Project-wide code-review conventions — `pr-review-toolkit:code-reviewer` owns those
