Integrated into release/v3.8.8. Applied review fixes: moved the SELECT 1 into a pingDb() db helper (no raw SQL in route, Hard Rule #5) + the 503 catch no longer leaks err.message (Hard Rule #12). Thanks @herjarsa!
14 KiB
name, description
| name | description |
|---|---|
| review-discussions-cc | Read all open GitHub Discussions, summarize them, respond to pending ones, create issues from actionable feature requests, and triage stale threads for closure |
/review-discussions — GitHub Discussions Review & Response Workflow
Overview
This workflow reads all open GitHub Discussions, generates a categorized summary, identifies which ones need a response, drafts and posts replies, optionally creates issues from actionable feature requests, and triages stale threads for closure.
Modern tooling (replaces deprecated browser_subagent flow):
- Reads use
gh api graphql— one query returns 50 discussions with full bodies, comments, replies, IDs, andupdatedAt. - Writes (post comment, create issue, close discussion) use
gh api graphqlmutations orgh issue create. - Pace at ~1s between writes to avoid abuse-detection throttling.
WebFetchis acceptable only for read-only HTML scraping when GraphQL is unavailable — never for write actions.
// turbo-all
Steps
1. Identify the GitHub Repository
- Run:
git -C <project_root> remote get-url originto extractowner/repo. - Parse owner and repo name from the URL (https or ssh form).
2. Fetch All Open Discussions (paginated GraphQL)
GraphQL caps each discussions query at 50 nodes — repos with more than 50 open discussions must paginate. Loop with first: 50, after: $cursor until pageInfo.hasNextPage is false. Skipping pagination silently drops the older half of the backlog, which is exactly where most stale-candidates and unanswered follow-ups live (regression observed 2026-05-28: page-1-only fetch missed 5 follow-ups and 4 stale candidates ranging from 23d to 56d).
Each page request must return the same field set — easy mistake is to fetch page 2 without body (because the cursor query was hand-edited). Define one query string with body on both the discussion and every comment/reply, and reuse it across pages.
Critical fields per discussion: id (node ID, not the visible number), number, title, url, createdAt, updatedAt, author.login, category.name, body, answerChosenAt, labels(first: 10) { nodes { name } }, plus nested comments(first: 50) { totalCount, nodes { id, author.login, body, createdAt, replies(first: 20) { nodes { author.login, body, createdAt } } } }. Must also include pageInfo { hasNextPage endCursor } on the discussions connection.
Persist the merged result (all pages concatenated) to /tmp/discussions-<repo>-<date>.json so re-runs in the same session avoid a re-fetch. Build an id → number map for the post phase — the GraphQL addDiscussionComment mutation requires the node ID, not the number.
Capture image attachments present in body or comments (<img src="..."> or markdown ). Surface their count in the per-discussion summary (e.g., 📷 3 screenshots) so the user can decide if visual context matters before approving a draft.
3. Summarize All Discussions
For each discussion, extract:
- Title and #Number
- Author (GitHub username)
- Category (Announcements, General, Ideas, Q&A, Show and tell)
- Created + Last updated (ISO date)
- Summary of original post (1-2 sentences)
- Comment count + last commenter + last comment date — determine these by chronological
createdAt, not iteration order. Comments and their nested replies must be merged into a single sorted timeline before picking the latest event (otherwise a recent top-level reply gets shadowed by an older nested reply of an earlier comment, and the discussion is misclassified). - Maintainer involvement: whether the repo owner already replied, and how many times
- Pending action — derived state, see categories below
- Attachments: count of screenshots / videos / pastebin links
- Detected language of the reporter (for reply-language matching)
4. Present Summary Report to User
Group by pending action, not by category, so the human sees triage buckets at a glance:
| State | Meaning |
|---|---|
| ⚠️ Needs first response | Zero comments, or all comments are from non-maintainers |
| 🔄 Follow-up pending | Maintainer replied, but reporter or third party added a new comment maintainer has not addressed |
| 🕒 Stale (>15d) | Maintainer was last to comment, no activity for 15+ days — candidate for soft-close or reporter-ping (see step 8) |
| ✅ Answered | Maintainer already replied AND last commenter is the maintainer AND age < 15d |
| 🏁 Resolved | answerChosenAt is set |
Within each bucket, present a table:
| # | Category | Title | Author | Updated | Notes |
|---|---|---|---|---|---|
| #N | Q&A | short title (60ch) | @user | YYYY-MM-DD | 📷2 · 🐛bug · 💡FR |
Tag rows with content hints when detected: 🐛bug ([BUG] / error / stacktrace in body), 💡FR (feature request / add support for), ❓support (config/usage question), 🙏thanks (short ack-only follow-up).
5. Draft & Post Responses
Reply templates by intent
Pick the template that matches the discussion intent — do NOT use a single generic format.
A. Bug confirmed — ack + root cause + tracking + workaround
Hey @user! Confirmed -- {root cause in one sentence}. I traced it to `path/to/file.ts:line`.
{Why it happens: 2-4 sentences of technical detail}
I have opened {issue #N} to track the fix. Workaround until it ships: {concrete steps}. Will update here when the patch lands.
B. Feature Request — ack + status + scope + commit
Hey @user! {Status: "Already exists" / "Tracked in #N" / "Reasonable, opening an issue"}.
{If already exists: pointer to dashboard page or doc}
{If tracked: link to umbrella, summarize order/priority}
{If new: open issue + post link back}
{Optional: short technical note on feasibility / trade-offs}
C. Support / config question — direct answer + reference + offer to dig deeper
Hey @user! {One-sentence answer}.
Steps:
1. ...
2. ...
3. ...
Reference: `docs/<path>.md`. If it still fails after that, paste {specific thing} and I will trace it.
D. Thank-you / short follow-up — 1-2 sentences
Glad it helps, @user! {Concrete next marker — when patch ships / when to expect next update}.
E. Stale / closing — see step 8
Posting via gh (replaces deprecated browser flow)
gh api graphql -f query='
mutation($id: ID!, $body: String!) {
addDiscussionComment(input: {discussionId: $id, body: $body}) {
comment { id url }
}
}' -f id="$NODE_ID" -f body="$BODY"
For threaded replies (recommended when responding to a specific comment in a long thread), add replyToId: $parentCommentId to the input.
Output hygiene (still applies even via API — the comment renders in GitHub UI):
- ASCII-safe punctuation: regular hyphens
-,->for arrows - Markdown OK:
**bold**, fenced code blocks,[text](url)links - No bare error messages with stack traces from internal logs — sanitize
- Match reporter's language (pt-BR reporter → pt-BR reply; ru reporter → ru reply); default to English when uncertain
Pacing: sleep 1 between mutations. GitHub abuse-detection trips around 10/sec for the same actor.
Verification: capture the returned comment.url from each mutation. Failed posts (returncode != 0 or errors in response) get logged separately and retried once after a 5s pause.
6. Create Issues from Actionable Feature Requests
For discussions that contain concrete, actionable feature requests:
-
Deduplicate FIRST — before drafting, search existing issues:
gh issue list --repo $OWNER/$REPO --search "<keywords from FR>" --state open --json number,title,labelsIf a matching issue (or umbrella) already exists, reuse it — never create a duplicate. Post a comment in the discussion linking to the existing issue.
-
Ask the user which to create — even after dedup, the human approves the final list.
-
Create the issue with
gh issue create:gh issue create --repo $OWNER/$REPO \ --title "[feature] <short imperative>" \ --label enhancement \ --body @/tmp/issue-body.mdBody template:
## Feature Request **Source:** Discussion #N by @author ## Problem What limitation the user hit (in their words, paraphrased) ## Proposed Solution How it could work ### Implementation Ideas - File paths likely to touch (use `Grep` if needed to confirm) - Related modules / patterns already in the codebase ### Current Workarounds What users can do today ## Additional Context - Discussion: #N - Related issues/PRs: #X, #Y - Upstream references: link to similar implementations in `_references/` if applicable -
Generate task file in
_ideia/when the feature needs deeper investigation before implementation:_ideia/<short-kebab-slug>.mdContains: problem statement, current OmniRoute state, how upstream (
_references/9router,_references/CLIProxyAPI, etc.) handles it, proposed implementation levels (short/medium/long term), acceptance criteria. -
Link back to discussion with the real URL:
Follow-up @reporter — I've opened issue #N to track this. {1-line summary of what the issue covers}.
7. Final Report
| Discussion | Action Taken |
|---|---|
| #N — Title | Responded (bug confirmed, tracking #M) |
| #N — Title | Responded + created issue #M + task file _ideia/X.md |
| #N — Title | Responded (support answered with workaround) |
| #N — Title | Responded to follow-up comment |
| #N — Title | Closed (stale 15+d, no reply from reporter) |
| #N — Title | Ping sent (stale 15+d, will close in 7d if no response) |
Include totals: comments posted, issues created, discussions closed, discussions pinged. Capture median response time for the batch.
8. Stale Discussion Triage (auto-close candidates)
Identify discussions matching all of:
updatedAt > 15 days ago- Maintainer already replied at least once
- Last commenter is the maintainer (the ball is on the reporter's side)
answerChosenAtis null (not formally resolved)- Category in
{Q&A, General}— skipIdeas/Show and tell/Announcements(those serve as community references and shouldn't be closed) comments.totalCount >= 2— there was actual conversation, not a drive-by post- No label named
keep-open(escape hatch)
For each candidate, present to the user with a recommended action:
| Action | When |
|---|---|
| Soft-close | Default — maintainer answered concretely and reporter went silent |
| Ping reporter | Maintainer asked for more info (log dump, screenshot) and never got it |
| Keep open | Conversation is mid-debug and closing would lose context — operator override |
Soft-close mutation:
gh api graphql -f query='
mutation($id: ID!) {
closeDiscussion(input: {discussionId: $id, reason: RESOLVED}) {
discussion { id closed }
}
}' -f id="$NODE_ID"
Valid reason values: RESOLVED, OUTDATED, DUPLICATE. Default to OUTDATED for "no response" closures, RESOLVED for answered-but-not-confirmed.
Before closing, post a closing comment:
Closing for inactivity -- feel free to reopen if you still hit this, or open a fresh issue with a current log. Thanks!
Ping flow (alternative):
@reporter -- still happening on the latest version? Otherwise I'll close this in 7 days for inactivity.
Persist the ping in _cache/discussions-pinged-<date>.json so the next run knows to close discussions that were pinged 7+ days ago without a reply.
Notes
- This workflow is interactive — always present the summary and wait for user approval before posting responses, creating issues, or closing discussions.
- Use
AskUserQuestionto gather batched approval — separate questions for "reply scope", "create issues for?", "close stale?". Stale handling is a separate consent from reply posting. - For discussions in non-English languages (
pt-BR,ru,zh,es), respond in the same language as the original post. Default to English when uncertain. - Always reference specific dashboard paths, config options, doc files, or code locations (
file:line) when explaining existing features — never wave hands. - When a discussion reveals a bug, separate it from feature requests in the report. Bugs need a tracking issue + workaround; FRs need scoping.
- Before recommending a workaround that mentions a file/flag/setting, verify it exists in the current codebase (the previous turn's memory may be stale).
- Trust-but-verify: after a batch post, spot-check 2-3 random
comment.urlreturns in the browser to confirm the comments rendered cleanly (no Unicode mojibake, no broken markdown). - Secure-by-default guidance (tldrsec/awesome-secure-defaults): when responses recommend security-relevant code (auth, crypto, SSRF, XSS sanitization), prefer well-tested libraries (Helmet.js, DOMPurify, Google Tink, ssrf-req-filter, safe-regex) over hand-rolled solutions.
Anti-patterns to avoid
- ❌ Posting via
browser_subagentclicks — slow, flaky, and obsolete sincegh api graphqlmutations exist. - ❌ N+1 fetches (one per discussion) — use one GraphQL query for all 50.
- ❌ Creating an issue without checking for an existing umbrella / similar one first.
- ❌ Generic "thanks, I'll look into it!" responses — every reply must reference a file, doc, or concrete action.
- ❌ Closing a stale discussion without posting a closing comment first.
- ❌ Skipping the user approval gate ("turbo-all" never bypasses interactive consent for writes).