--- 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 ; 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 | head -150` or `gh pr view --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 # on LanternOps/breeze. Fetch the diff with: gh pr view --repo LanternOps/breeze --json title,body,additions,deletions,files gh pr diff --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-.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 --repo LanternOps/breeze --request-changes --body "$(cat <<'EOF' **Root cause:** ... EOF )" gh issue comment --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:){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/.mdx` → `https://docs.breezermm.com//` (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 --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= --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 --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 --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:){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 --repo LanternOps/breeze --comments --json title,body,comments,author,labels # Author history gh search prs --author --repo LanternOps/breeze --limit 20 # Merge (enqueue — the merge queue owns the squash; never --admin) gh pr merge --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