Files
OmniRoute/.agents/skills/review-discussions-ag/SKILL.md
Hernan Javier Ardila Sanchez fd26e601a2 fix(dashboard): use lightweight ping endpoint for MaintenanceBanner (fixes #3040) (#3043)
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!
2026-06-01 14:30:17 -03:00

272 lines
14 KiB
Markdown

---
name: review-discussions-ag
description: 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, and `updatedAt`.
- Writes (post comment, create issue, close discussion) use `gh api graphql` mutations or `gh issue create`.
- Pace at ~1s between writes to avoid abuse-detection throttling.
- `WebFetch` is 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 origin` to extract `owner/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)
```bash
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:
1. **Deduplicate FIRST** — before drafting, search existing issues:
```bash
gh issue list --repo $OWNER/$REPO --search "<keywords from FR>" --state open --json number,title,labels
```
If a matching issue (or umbrella) already exists, reuse it — never create a duplicate. Post a comment in the discussion linking to the existing issue.
2. **Ask the user which to create** — even after dedup, the human approves the final list.
3. **Create the issue** with `gh issue create`:
```bash
gh issue create --repo $OWNER/$REPO \
--title "[feature] <short imperative>" \
--label enhancement \
--body @/tmp/issue-body.md
```
Body template:
```markdown
## 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
- 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
```
4. **Generate task file in `_ideia/`** when the feature needs deeper investigation before implementation:
```
_ideia/<short-kebab-slug>.md
```
Contains: problem statement, current OmniRoute state, how upstream (`_references/9router`, `_references/CLIProxyAPI`, etc.) handles it, proposed implementation levels (short/medium/long term), acceptance criteria.
5. **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)
- `answerChosenAt` is null (not formally resolved)
- Category in `{Q&A, General}` — skip `Ideas` / `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:**
```bash
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.
- Gather batched approval — separate consents for "reply scope", "create issues for?", "close stale?". Stale handling is a distinct 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.url` returns in the browser to confirm the comments rendered cleanly (no Unicode mojibake, no broken markdown).
## Anti-patterns to avoid
- ❌ Posting via `browser_subagent` clicks — slow, flaky, and obsolete since `gh api graphql` mutations 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).