feat: maintainer toolset β scheduled triage sweep + daily Discord scorecard#950
feat: maintainer toolset β scheduled triage sweep + daily Discord scorecard#950tombeckenham wants to merge 4 commits into
Conversation
β¦recard Adds two stateless, API-only scheduled workflows driven by a fixture-tested TypeScript toolkit in scripts/maintainer/: - maintainer-sweep.yml (every 3h): routes and assigns open PRs/issues to maintainers via .github/maintainers.json area globs with least-loaded rotation and per-maintainer caps, posts a one-time ack comment with a deterministic pre-review checklist (CI, conflicts, changeset, E2E), requests reproductions on bug reports lacking one, and reconciles waiting-on / ready-to-merge / needs-repro / has-pr labels. Suspected drive-by/bounty PRs (new account + trivial diff + no linked issue) are left untouched for human judgment. - maintainer-scorecard.yml (daily): posts a to-do digest to Discord β SLA breaches (24h first response / 48h follow-up), ready-to-merge PRs, per-maintainer queues with fresh contributor replies, new/flagged/stale items, unanswered discussions, same-author PR clusters, and stats (open deltas, first-response median/p90, merges, pending changesets). All metrics are recomputed from GitHub timelines each run (no stored state); idempotency comes from HTML markers in bot comments plus visible assignment/label state. PR code is never checked out or executed β no pull_request_target. Unit tests wire into CI via the root test:maintainer nx target. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
π WalkthroughWalkthroughIntroduces a maintainer automation toolset that collects GitHub repository data, classifies and routes work, plans or executes sweep mutations, computes scorecards, publishes Discord digests, and runs through scheduled or manual GitHub Actions workflows. ChangesMaintainer automation
Estimated code review effort: 5 (Critical) | ~120 minutes π₯ Pre-merge checks | β 4 | β 1β Failed checks (1 warning)
β Passed checks (4 passed)
β¨ Finishing Touchesπ Generate docstrings
π§ͺ Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
π Changeset Version Preview19 package(s) bumped directly, 26 bumped as dependents. π₯ Major bumps
π¨ Minor bumps
π© Patch bumps
|
|
View your CI Pipeline Execution β for commit 5169c28
βοΈ Nx Cloud last updated this comment at |
@tanstack/ai
@tanstack/ai-acp
@tanstack/ai-angular
@tanstack/ai-anthropic
@tanstack/ai-bedrock
@tanstack/ai-claude-code
@tanstack/ai-client
@tanstack/ai-code-mode
@tanstack/ai-code-mode-skills
@tanstack/ai-codex
@tanstack/ai-devtools-core
@tanstack/ai-elevenlabs
@tanstack/ai-event-client
@tanstack/ai-fal
@tanstack/ai-gemini
@tanstack/ai-grok
@tanstack/ai-grok-build
@tanstack/ai-groq
@tanstack/ai-isolate-cloudflare
@tanstack/ai-isolate-node
@tanstack/ai-isolate-quickjs
@tanstack/ai-mcp
@tanstack/ai-mistral
@tanstack/ai-ollama
@tanstack/ai-openai
@tanstack/ai-opencode
@tanstack/ai-openrouter
@tanstack/ai-preact
@tanstack/ai-react
@tanstack/ai-react-ui
@tanstack/ai-sandbox
@tanstack/ai-sandbox-cloudflare
@tanstack/ai-sandbox-daytona
@tanstack/ai-sandbox-docker
@tanstack/ai-sandbox-local-process
@tanstack/ai-sandbox-sprites
@tanstack/ai-sandbox-vercel
@tanstack/ai-solid
@tanstack/ai-solid-ui
@tanstack/ai-svelte
@tanstack/ai-utils
@tanstack/ai-vue
@tanstack/ai-vue-ui
@tanstack/openai-base
@tanstack/preact-ai-devtools
@tanstack/react-ai-devtools
@tanstack/solid-ai-devtools
commit: |
β¦ation The team doesn't split by code area, so drop the guessed area globs from maintainers.json. Assignment is now pure least-loaded rotation; `areas` stays supported as an optional per-maintainer routing priority. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Issue/PR titles are attacker-controlled and were interpolated verbatim into Discord embed descriptions, so a title like "[urgent: click](https://evil.com)" rendered as a masked link in the maintainer channel. shortTitle() now backslash-escapes Discord markdown metacharacters after truncation; the bot's own [#N](url) links are built separately and still render. Also simplifies a redundant type assertion in the GraphQL error check. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 13
π€ Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In @.github/workflows/maintainer-scorecard.yml:
- Around line 30-31: Update permissions in
.github/workflows/maintainer-scorecard.yml at lines 30-31 to add issues: read,
pull-requests: read, and discussions: read alongside contents: read. Update
permissions in .github/workflows/maintainer-sweep.yml at lines 28-31 to add
discussions: read while preserving its existing issues: write and pull-requests:
write permissions.
In `@scripts/maintainer/classify.ts`:
- Around line 170-176: Update changesRequestedOutstanding to use the latest
CHANGES_REQUESTED review and compare only PR-author comments or commits
occurring after that review; stop relying on aggregate lastContributorActivityAt
and lastMaintainerResponseAt timestamps. Preserve the outstanding state when no
author activity follows the requesting review, and add regression coverage for
intervening third-party and maintainer comments.
- Around line 165-168: Update the readyToMerge calculation in
scripts/maintainer/classify.ts to require explicitly successful CI and
explicitly mergeable pull-request states, rather than only excluding failure and
conflict states. Preserve the existing approval and non-draft requirements, and
add negative tests covering pending CI and unknown mergeability.
- Around line 135-142: Update hasMarkerComment to consider a marker only when
the timeline event is an automation-generated comment, while preserving the
existing marker-body matching. Use the eventβs existing author or automation
identity field to exclude contributor comments before checking
e.body.includes(marker).
In `@scripts/maintainer/collect.ts`:
- Around line 125-130: Update the snapshot collection logic around the GraphQL
connections files, closingIssuesReferences, timelineItems, and discussion
comments so capped results are not treated as complete data. Propagate each
connectionβs pagination/completeness metadata through the collected snapshot,
and ensure downstream triage automation either fetches all pages or suppresses
decisions when any required connection is truncated.
In `@scripts/maintainer/config.ts`:
- Around line 18-20: Update the validation guarding repo in the configuration
flow to require exactly one slash separating two non-empty owner and name
components, rejecting values such as owner/, /repo, and owner/repo/extra before
collectSnapshot receives them.
In `@scripts/maintainer/discord.ts`:
- Around line 201-209: Bound the webhook request in the chunkEmbeds loop by
adding an AbortController-based timeout to the fetchImpl call, passing its
signal and clearing the timer when the request settles. Handle timeout or abort
failures explicitly so the scheduled workflow exits through the intended failure
path, and add coverage for the stalled-request case.
- Around line 201-209: Update the webhook payload in the embed-sending loop to
place maintainer mentions in the top-level content field rather than only inside
embed descriptions. Use the existing mention-related symbols to build that
content for alerting messages, and set allowed_mentions.users to exactly the
corresponding maintainer IDs while preserving the existing embed chunking and
POST behavior.
- Around line 116-123: Update the embed construction around flaggedLines and the
βNew in the last 24hβ title to place flagged entries before newLines when
building the section, and include the flagged count in the title alongside the
new item count. Preserve the existing flagged-line formatting and section
truncation behavior.
In `@scripts/maintainer/github.ts`:
- Around line 15-19: Update GitHubClientOptions and the request paths in the
GitHub client to support an injectable timeout, using it to abort every GitHub
fetch when the deadline expires. Ensure the timeout applies consistently to both
API paths while preserving the existing fetchImpl and baseUrl behavior.
In `@scripts/maintainer/metrics.ts`:
- Around line 238-246: Update the assignment-coverage calculation around
assignablePRs and pctPRsAssigned to exclude PRs marked suspectedSpam from the
denominator, while preserving the existing draft exclusion and assigned-count
logic. In scripts/maintainer/README.md lines 5-9, clarify that assignment
applies only to eligible items and document the cap/flag exceptions.
- Around line 224-235: Update the recentlyClosed loop to exclude items whose
author matches the configured roster, using the same case-insensitive
rosterAuthor check already applied to open samples. Extend the existing
early-continue condition alongside authorIsBot and the weekAgo cutoff, while
preserving response state derivation for eligible items.
- Around line 126-131: Update the freshReplies filter to require an explicit
contributor-after-maintainer transition for both pull requests and issues,
rather than treating every item with waitingOn === 'maintainer' as fresh. Use
the existing freshContributorReply state where available and add equivalent
transition tracking to the issue data before filtering; preserve only items that
actually represent a new contributor reply.
πͺ Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
βΉοΈ Review info
βοΈ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 6551ce8e-7d7d-43cd-a602-daf6e7ef787f
π Files selected for processing (24)
.github/maintainers.json.github/workflows/maintainer-scorecard.yml.github/workflows/maintainer-sweep.ymlpackage.jsonscripts/maintainer/README.mdscripts/maintainer/actions.tsscripts/maintainer/classify.test.tsscripts/maintainer/classify.tsscripts/maintainer/collect.tsscripts/maintainer/comments.tsscripts/maintainer/config.tsscripts/maintainer/discord.tsscripts/maintainer/env.tsscripts/maintainer/fixtures.tsscripts/maintainer/github.tsscripts/maintainer/glob.tsscripts/maintainer/metrics.test.tsscripts/maintainer/metrics.tsscripts/maintainer/route.test.tsscripts/maintainer/route.tsscripts/maintainer/scorecard.tsscripts/maintainer/sweep.test.tsscripts/maintainer/sweep.tsscripts/maintainer/types.ts
| permissions: | ||
| contents: read |
There was a problem hiding this comment.
π©Ί Stability & Availability | π Major | β‘ Quick win
π§© Analysis chain
π Script executed:
#!/bin/bash
set -euo pipefail
# Inspect the two workflows around the referenced lines.
for f in .github/workflows/maintainer-scorecard.yml .github/workflows/maintainer-sweep.yml; do
echo "===== $f ====="
nl -ba "$f" | sed -n '1,220p'
echo
done
# Look for code that collects issues, pull requests, discussions, or scorecard/sweep snapshot data.
rg -n "discussion|discussions|pull-requests|issues|snapshot|scorecard|sweep" .github/workflows . --glob '!**/node_modules/**'Repository: TanStack/ai
Length of output: 244
π Script executed:
#!/bin/bash
set -euo pipefail
echo "=== maintainer-scorecard.yml ==="
sed -n '1,220p' .github/workflows/maintainer-scorecard.yml | cat -n
echo
echo "=== maintainer-sweep.yml ==="
sed -n '1,260p' .github/workflows/maintainer-sweep.yml | cat -n
echo
echo "=== workflow references ==="
rg -n "discussion|discussions|pull-requests|issues|scorecard|sweep|github\.api|octokit|GraphQL|REST" .github/workflows . --glob '!**/node_modules/**'Repository: TanStack/ai
Length of output: 50367
π Web query:
GitHub Actions workflow permissions write includes read discussions issues pull-requests repository permissions docs
π‘ Result:
In GitHub Actions, the permissions key allows you to define the access level of the GITHUB_TOKEN for a specific job or an entire workflow [1][2]. This is a security best practice that enables the principle of least privilege, ensuring the token has only the minimum access required to perform its tasks [1][3]. When you explicitly define the permissions key in your workflow file, all unspecified permissions are automatically set to none, except for the metadata scope, which always retains read access [2]. The available permission scopes include, but are not limited to, the following [4]: contents: Manages repository code access. The write level allows you to push commits, create tags, and manage releases. issues: Manages issue tracking. The write level allows you to create, edit, close, or delete issues and add comments. pull-requests: Manages pull request operations. The write level permits actions such as creating or merging pull requests, as well as modifying PR labels, assignees, and descriptions. discussions: Manages GitHub Discussions. The write level allows you to create, edit, or delete discussions and comments. actions: Manages workflow execution and control. checks: Manages status checks and build results. deployments: Manages deployment status and infrastructure. packages: Manages GitHub Packages. pages: Manages GitHub Pages settings and publishing. When granting write access, the token is granted the ability to modify or delete existing data within that specific scope, whereas read access only permits viewing that data [2]. For example, contents: write enables the token to push changes to the repository, which is necessary for tasks like automated version bumping or generating documentation [5][2]. For security, it is recommended to audit and limit these permissions, as broad default settings (if configured at the repository or organization level) may grant more access than your workflow requires [3]. You can manage these settings at the job or workflow level in your YAML configuration [2].
Citations:
- 1: https://github.com/github/docs/blob/main/content/actions/tutorials/authenticate-with-github_token.md
- 2: https://dev.to/github/the-githubtoken-in-github-actions-how-it-works-change-permissions-customizations-3cgp
- 3: https://github.blog/security/new-tool-to-secure-your-github-actions/
- 4: https://github.github.com/gh-aw/reference/permissions/
- 5: https://stackoverflow.com/questions/72110199/what-is-contents-write-permission-in-github-workflow
π Script executed:
#!/bin/bash
set -euo pipefail
echo "=== scripts/maintainer/collect.ts (relevant sections) ==="
sed -n '1,220p' scripts/maintainer/collect.ts | cat -n
echo
sed -n '220,420p' scripts/maintainer/collect.ts | cat -n
echo
echo "=== scripts/maintainer/README.md (permissions / API notes) ==="
sed -n '1,120p' scripts/maintainer/README.md | cat -n
echo
echo "=== scripts/maintainer/sweep.ts (high-level data usage) ==="
sed -n '1,240p' scripts/maintainer/sweep.ts | cat -n
echo
echo "=== scripts/maintainer/scorecard.ts (high-level data usage) ==="
sed -n '1,140p' scripts/maintainer/scorecard.ts | cat -nRepository: TanStack/ai
Length of output: 30056
Grant the repo read scopes snapshot collection needs
collectSnapshot() reads open PRs, issues, and discussions. maintainer-scorecard.yml only grants contents: read, so add issues: read, pull-requests: read, and discussions: read there; maintainer-sweep.yml already has issues: write and pull-requests: write, so it only needs discussions: read.
π Affects 2 files
.github/workflows/maintainer-scorecard.yml#L30-L31(this comment).github/workflows/maintainer-sweep.yml#L28-L31
π€ Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In @.github/workflows/maintainer-scorecard.yml around lines 30 - 31, Update
permissions in .github/workflows/maintainer-scorecard.yml at lines 30-31 to add
issues: read, pull-requests: read, and discussions: read alongside contents:
read. Update permissions in .github/workflows/maintainer-sweep.yml at lines
28-31 to add discussions: read while preserving its existing issues: write and
pull-requests: write permissions.
| function hasMarkerComment( | ||
| timeline: Array<TimelineEvent>, | ||
| marker: string, | ||
| ): boolean { | ||
| return timeline.some( | ||
| (e) => | ||
| e.kind === 'comment' && e.body !== undefined && e.body.includes(marker), | ||
| ) |
There was a problem hiding this comment.
π― Functional Correctness | π‘ Minor | β‘ Quick win
Only recognize markers in automation comments.
Any contributor can currently include ACK_MARKER or REPRO_MARKER and suppress the corresponding action.
Proposed fix
return timeline.some(
(e) =>
- e.kind === 'comment' && e.body !== undefined && e.body.includes(marker),
+ e.kind === 'comment' &&
+ e.isBot &&
+ e.body !== undefined &&
+ e.body.includes(marker),
)π Committable suggestion
βΌοΈ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| function hasMarkerComment( | |
| timeline: Array<TimelineEvent>, | |
| marker: string, | |
| ): boolean { | |
| return timeline.some( | |
| (e) => | |
| e.kind === 'comment' && e.body !== undefined && e.body.includes(marker), | |
| ) | |
| function hasMarkerComment( | |
| timeline: Array<TimelineEvent>, | |
| marker: string, | |
| ): boolean { | |
| return timeline.some( | |
| (e) => | |
| e.kind === 'comment' && | |
| e.isBot && | |
| e.body !== undefined && | |
| e.body.includes(marker), | |
| ) |
π€ Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@scripts/maintainer/classify.ts` around lines 135 - 142, Update
hasMarkerComment to consider a marker only when the timeline event is an
automation-generated comment, while preserving the existing marker-body
matching. Use the eventβs existing author or automation identity field to
exclude contributor comments before checking e.body.includes(marker).
| const hasConflicts = pr.mergeable === 'CONFLICTING' | ||
| const ciFailing = pr.ciState === 'failure' | ||
| const approved = pr.reviewDecision === 'APPROVED' | ||
| const readyToMerge = approved && !hasConflicts && !ciFailing && !pr.isDraft |
There was a problem hiding this comment.
π― Functional Correctness | π Major | β‘ Quick win
Require explicitly successful CI and mergeability before marking ready.
Line 168 treats pending/unknown CI and mergeability as ready because it only excludes known failure and conflict states. This can publish non-mergeable PRs in the ready queue.
Proposed fix
const approved = pr.reviewDecision === 'APPROVED'
-const readyToMerge = approved && !hasConflicts && !ciFailing && !pr.isDraft
+const readyToMerge =
+ approved &&
+ pr.mergeable === 'MERGEABLE' &&
+ pr.ciState === 'success' &&
+ !pr.isDraftAdd negative tests for pending CI and unknown mergeability.
π Committable suggestion
βΌοΈ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const hasConflicts = pr.mergeable === 'CONFLICTING' | |
| const ciFailing = pr.ciState === 'failure' | |
| const approved = pr.reviewDecision === 'APPROVED' | |
| const readyToMerge = approved && !hasConflicts && !ciFailing && !pr.isDraft | |
| const hasConflicts = pr.mergeable === 'CONFLICTING' | |
| const ciFailing = pr.ciState === 'failure' | |
| const approved = pr.reviewDecision === 'APPROVED' | |
| const readyToMerge = | |
| approved && | |
| pr.mergeable === 'MERGEABLE' && | |
| pr.ciState === 'success' && | |
| !pr.isDraft |
π€ Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@scripts/maintainer/classify.ts` around lines 165 - 168, Update the
readyToMerge calculation in scripts/maintainer/classify.ts to require explicitly
successful CI and explicitly mergeable pull-request states, rather than only
excluding failure and conflict states. Preserve the existing approval and
non-draft requirements, and add negative tests covering pending CI and unknown
mergeability.
| // Outstanding changes-requested: the decision stands and the contributor | ||
| // hasn't commented or pushed since the last maintainer response. | ||
| const changesRequestedOutstanding = | ||
| pr.reviewDecision === 'CHANGES_REQUESTED' && | ||
| (state.lastContributorActivityAt === null || | ||
| (state.lastMaintainerResponseAt !== null && | ||
| state.lastContributorActivityAt <= state.lastMaintainerResponseAt)) |
There was a problem hiding this comment.
π― Functional Correctness | π Major | β‘ Quick win
Base outstanding change requests on the requesting review and PR author.
These aggregate timestamps conflate unrelated participants and maintainer comments with the review lifecycle. For example, an author can push after a change request, then a later maintainer comment makes the PR wait on the author again while reviewDecision remains CHANGES_REQUESTED.
Compare author comments/commits against the latest changes-requested review instead, and add regression coverage for intervening third-party and maintainer comments.
π€ Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@scripts/maintainer/classify.ts` around lines 170 - 176, Update
changesRequestedOutstanding to use the latest CHANGES_REQUESTED review and
compare only PR-author comments or commits occurring after that review; stop
relying on aggregate lastContributorActivityAt and lastMaintainerResponseAt
timestamps. Preserve the outstanding state when no author activity follows the
requesting review, and add regression coverage for intervening third-party and
maintainer comments.
| labels(first: 20) { nodes { name } } | ||
| assignees(first: 10) { nodes { login } } | ||
| files(first: 100) { nodes { path } } | ||
| closingIssuesReferences(first: 10) { nodes { number } } | ||
| commits(last: 1) { nodes { commit { statusCheckRollup { state } } } } | ||
| timelineItems(last: 100, itemTypes: [ISSUE_COMMENT, PULL_REQUEST_REVIEW, PULL_REQUEST_COMMIT]) { |
There was a problem hiding this comment.
ποΈ Data Integrity & Integration | π Major | ποΈ Heavy lift
π§© Analysis chain
π Script executed:
#!/bin/bash
set -euo pipefail
# Locate the collector and relevant consumers
git ls-files | rg '^(scripts/maintainer/collect\.ts|src/|scripts/)' || true
echo '--- collect.ts outline ---'
ast-grep outline scripts/maintainer/collect.ts --view expanded || true
echo '--- search for normalized completeness metadata / truncation handling ---'
rg -n "complete|completeness|truncat|pageInfo|hasNextPage|labels\\(|assignees\\(|files\\(|closingIssuesReferences\\(|timelineItems\\(" scripts src -g '!**/node_modules/**' || trueRepository: TanStack/ai
Length of output: 4588
π Script executed:
#!/bin/bash
set -euo pipefail
sed -n '1,240p' scripts/maintainer/collect.tsRepository: TanStack/ai
Length of output: 7179
π Script executed:
#!/bin/bash
set -euo pipefail
echo '--- types.ts outline ---'
ast-grep outline scripts/maintainer/types.ts --view expanded || true
echo '--- classify.ts outline ---'
ast-grep outline scripts/maintainer/classify.ts --view expanded || true
echo '--- collect.ts relevant remainder ---'
sed -n '240,420p' scripts/maintainer/collect.ts
echo '--- search for files/labels/timeline consumers ---'
rg -n "\.files\b|\.labels\b|\.assignees\b|timeline|closingIssuesReferences|changedFiles|isAnswered|upvoteCount|statusCheckRollup" scripts/maintainer -g '!**/*.test.ts' || trueRepository: TanStack/ai
Length of output: 15367
π Script executed:
#!/bin/bash
set -euo pipefail
echo '--- types.ts outline ---'
ast-grep outline scripts/maintainer/types.ts --view expanded || true
echo '--- classify.ts outline ---'
ast-grep outline scripts/maintainer/classify.ts --view expanded || true
echo '--- collect.ts remainder ---'
sed -n '240,420p' scripts/maintainer/collect.ts
echo '--- downstream consumers of snapshot fields ---'
rg -n "\b(files|labels|assignees|timeline|comments|closingIssuesReferences|changedFiles|isDraft|reviewDecision|statusCheckRollup)\b" scripts/maintainer/classify.ts scripts/maintainer/route.ts scripts/maintainer/actions.ts scripts/maintainer/scorecard.ts scripts/maintainer/sweep.ts scripts/maintainer/metrics.ts scripts/maintainer/comments.ts scripts/maintainer/discord.ts scripts/maintainer/types.ts || trueRepository: TanStack/ai
Length of output: 16999
π Script executed:
#!/bin/bash
set -euo pipefail
sed -n '1,220p' scripts/maintainer/classify.ts
echo '---'
sed -n '220,390p' scripts/maintainer/classify.ts
echo '---'
sed -n '1,140p' scripts/maintainer/comments.ts
echo '---'
sed -n '1,220p' scripts/maintainer/route.tsRepository: TanStack/ai
Length of output: 17652
Don't treat capped nested connections as complete snapshots. files, closingIssuesReferences, timelineItems, and discussion comments are all truncated here, but downstream triage uses them directly; that can hide changesets/E2E coverage, miss maintainer replies or marker comments, and undercount linked issues. Carry completeness metadata through the snapshot or suppress automation when a connection is truncated.
π€ Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@scripts/maintainer/collect.ts` around lines 125 - 130, Update the snapshot
collection logic around the GraphQL connections files, closingIssuesReferences,
timelineItems, and discussion comments so capped results are not treated as
complete data. Propagate each connectionβs pagination/completeness metadata
through the collected snapshot, and ensure downstream triage automation either
fetches all pages or suppresses decisions when any required connection is
truncated.
| for (const chunk of chunkEmbeds(embeds)) { | ||
| const response = await fetchImpl(webhookUrl, { | ||
| method: 'POST', | ||
| headers: { 'content-type': 'application/json' }, | ||
| body: JSON.stringify({ | ||
| embeds: chunk, | ||
| allowed_mentions: { parse: ['users'] }, | ||
| }), | ||
| }) |
There was a problem hiding this comment.
π©Ί Stability & Availability | π Major | β‘ Quick win
Bound the Discord webhook request.
Without an abort signal or timeout, a stalled network request can hold the scheduled workflow until its job timeout. Add a bounded request timeout and cover the failure path.
π€ Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@scripts/maintainer/discord.ts` around lines 201 - 209, Bound the webhook
request in the chunkEmbeds loop by adding an AbortController-based timeout to
the fetchImpl call, passing its signal and clearing the timer when the request
settles. Handle timeout or abort failures explicitly so the scheduled workflow
exits through the intended failure path, and add coverage for the
stalled-request case.
π― Functional Correctness | π Major | β‘ Quick win
π§© Analysis chain
π Script executed:
#!/bin/bash
set -euo pipefail
# Locate the reviewed file and relevant Discord payload construction
git ls-files scripts/maintainer/discord.ts
ast-grep outline scripts/maintainer/discord.ts --view expanded || true
# Read the surrounding lines around the reported snippet
sed -n '150,240p' scripts/maintainer/discord.ts
# Find any other Discord webhook/message payload usage in the repo
rg -n --hidden --no-ignore-vcs "allowed_mentions|embeds:|content:" scripts/maintainer scripts || trueRepository: TanStack/ai
Length of output: 4193
π Script executed:
#!/bin/bash
set -euo pipefail
# Inspect the embed construction and mention helper
sed -n '1,170p' scripts/maintainer/discord.ts
# Pull Discord webhook/message docs if available from the public docs page
python3 - <<'PY'
import urllib.request
url = "https://discord.com/developers/docs/resources/webhook#execute-webhook"
try:
with urllib.request.urlopen(url, timeout=20) as r:
data = r.read().decode("utf-8", "replace")
# Print only mention-related snippets
for needle in ["allowed_mentions", "embeds", "content", "mentions", "parse"]:
idx = data.lower().find(needle.lower())
if idx != -1:
start = max(0, idx - 250)
end = min(len(data), idx + 700)
print(f"\n--- {needle} ---\n")
print(data[start:end])
except Exception as e:
print(f"FETCH_ERROR: {e}")
PYRepository: TanStack/ai
Length of output: 5884
π Web query:
Discord webhook allowed_mentions embeds mentions content docs
π‘ Result:
The allowed_mentions object in the Discord webhook payload is a top-level property used to control which mentions (users, roles, or @everyone/@here) will trigger a notification [1][2]. Key technical points regarding allowed_mentions and embeds: Scope: The allowed_mentions object applies to the entire webhook message, including both the message content and any embeds [1]. It must be placed at the top level of the JSON payload; if placed inside an embed object, it will be ignored [1]. Functionality: It determines whether a mention string (e.g., <@123>, <@&456>, @everyone) renders as a functional mention that pings a user or role, or if it renders merely as plain text [2]. Mentions within Embeds: While you can include mention syntax (such as <@ID> or <@&ID>) in the text fields of an embed (like title or description), Discord does not support active pings within embeds [3]. These strings will typically render as text rather than clickable, notifying mentions regardless of the allowed_mentions configuration [3]. Configuration: 1. Parse Array: You can specify an array in the parse field containing "users", "roles", or "everyone" to allow auto-parsing for those categories [2]. 2. Whitelisting: Alternatively, you can use the users or roles arrays to whitelist specific IDs [1][2]. 3. Constraints: You cannot use the parse field and the explicit users/roles arrays in the same payload; doing so will result in a 400 Bad Request error [2]. For messages containing both content and embeds, a single top-level allowed_mentions object governs all mention parsing for the message [1]. Top results: [4][5][6][1][2][3]
Citations:
- 1: https://wisechecker.com/discord-webhook-allowed-mentions-whitelist/
- 2: https://discord-webhook.com/en/blog/discord-webhook-mentions/
- 3: @mention roles via discord webhookΒ containrrr/shoutrrr#295
- 4: https://docs.discord.com/developers/resources/webhook
- 5: https://docs.discord.com/developers/resources/message
- 6: https://github.com/discord/discord-api-docs/blob/8e844425/docs/resources/webhook.mdx
π Web query:
site:discord.com/developers/docs resources webhook allowed_mentions embeds content mention in embed no ping
π‘ Result:
To mention a user in a Discord webhook message without triggering a notification (ping), you should use the allowed_mentions object in your webhook payload. By setting the parse array to exclude users or roles, you can prevent the automatic pinging behavior that usually accompanies mentions. The allowed_mentions object allows you to control which mentions are parsed and result in a notification. To prevent all pings, you can use an empty parse array or explicitly exclude specific mention types. Example of an allowed_mentions payload structure: { "content": "Hello <@123456789012345678>", "allowed_mentions": { "parse": [] } } When sending content in an embed, the same allowed_mentions rules apply. Discord parses mentions within embed fields (such as title, description, and field values) based on the configuration provided in the allowed_mentions object. If you do not want the mention in the embed to ping the user, ensure that the allowed_mentions block is included in your request and that it does not explicitly allow user or role mentions (or keeps the parse array empty). Keep in mind that if you include a user ID in the mention format <@USER_ID> or <@!USER_ID> within your content or embed, Discord will render it as a mention. Using the empty parse array in allowed_mentions ensures that these mentions are treated as text only, preventing the associated notification.
Citations:
- 1: https://discord.com/developers/docs/social-sdk/classdiscordpp_1_1MessageHandle.html
- 2: https://discord.com/developers/docs/social-sdk/release_notes.html
- 3: https://discord.com/developers/docs/slayer-sdk/classdiscordpp_1_1MessageHandle.html
- 4: https://discord.com/developers/docs/social-sdk/namespacediscordpp.html
- 5: https://discord.com/developers/docs/slayer-sdk/classdiscordpp_1_1AdditionalContent.html
π Script executed:
#!/bin/bash
set -euo pipefail
# Show the embed builder and mention helper in full
cat -n scripts/maintainer/discord.ts | sed -n '1,170p'Repository: TanStack/ai
Length of output: 6927
Move maintainer mentions into top-level content. The mention() helper currently renders <@...> inside an embed description, and this webhook sends only embeds, so those maintainer pings wonβt notify anyone. Put the mentions in content for the message that should alert maintainers, and whitelist just those IDs in allowed_mentions.users.
π€ Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@scripts/maintainer/discord.ts` around lines 201 - 209, Update the webhook
payload in the embed-sending loop to place maintainer mentions in the top-level
content field rather than only inside embed descriptions. Use the existing
mention-related symbols to build that content for alerting messages, and set
allowed_mentions.users to exactly the corresponding maintainer IDs while
preserving the existing embed chunking and POST behavior.
| export interface GitHubClientOptions { | ||
| token: string | ||
| fetchImpl?: typeof fetch | ||
| baseUrl?: string | ||
| } |
There was a problem hiding this comment.
π©Ί Stability & Availability | π Major | β‘ Quick win
Add a timeout to every GitHub request.
Neither API path has a request deadline, so one stalled GitHub connection can hang the scheduled sweep or scorecard. Add an injectable timeout and abort the request when it expires.
Also applies to: 37-41, 62-67
π€ Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@scripts/maintainer/github.ts` around lines 15 - 19, Update
GitHubClientOptions and the request paths in the GitHub client to support an
injectable timeout, using it to abort every GitHub fetch when the deadline
expires. Ensure the timeout applies consistently to both API paths while
preserving the existing fetchImpl and baseUrl behavior.
| freshReplies: [...assignedPRs, ...assignedIssues].filter( | ||
| (t) => | ||
| ('freshContributorReply' in t | ||
| ? t.freshContributorReply | ||
| : t.waitingOn === 'maintainer') && t.waitingOn === 'maintainer', | ||
| ), |
There was a problem hiding this comment.
π― Functional Correctness | π Major | ποΈ Heavy lift
Do not treat every maintainer-waiting issue as a fresh reply.
For types without freshContributorReply, the fallback includes any item waiting on a maintainerβeven newly opened, never-answered issues. Track the contributor-after-maintainer transition for both PRs and issues before adding them to freshReplies.
π€ Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@scripts/maintainer/metrics.ts` around lines 126 - 131, Update the
freshReplies filter to require an explicit contributor-after-maintainer
transition for both pull requests and issues, rather than treating every item
with waitingOn === 'maintainer' as fresh. Use the existing freshContributorReply
state where available and add equivalent transition tracking to the issue data
before filtering; preserve only items that actually represent a new contributor
reply.
| for (const c of snapshot.recentlyClosed) { | ||
| if (c.authorIsBot || new Date(c.createdAt) < weekAgo) continue | ||
| const state = deriveResponseState( | ||
| c.timeline, | ||
| c.author, | ||
| c.createdAt, | ||
| config, | ||
| now, | ||
| ) | ||
| if (state.firstResponseHours !== null) { | ||
| responseSamples.push(state.firstResponseHours) | ||
| } |
There was a problem hiding this comment.
π― Functional Correctness | π‘ Minor | β‘ Quick win
Exclude roster-authored closed items from response samples.
Open samples exclude rosterAuthor, but closed samples only exclude bots. Maintainer-authored closed items can therefore skew the response percentiles. Apply a case-insensitive roster check here too.
π€ Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@scripts/maintainer/metrics.ts` around lines 224 - 235, Update the
recentlyClosed loop to exclude items whose author matches the configured roster,
using the same case-insensitive rosterAuthor check already applied to open
samples. Extend the existing early-continue condition alongside authorIsBot and
the weekAgo cutoff, while preserving response state derivation for eligible
items.
| const assignablePRs = humanPRs.filter((t) => !t.item.isDraft) | ||
| const pctPRsAssigned = | ||
| assignablePRs.length === 0 | ||
| ? 100 | ||
| : Math.round( | ||
| (assignablePRs.filter((t) => t.item.assignees.length > 0).length / | ||
| assignablePRs.length) * | ||
| 100, | ||
| ) |
There was a problem hiding this comment.
π― Functional Correctness | π‘ Minor | β‘ Quick win
Represent assignment eligibility consistently.
Suspected drive-by/bounty PRs are intentionally excluded from automatic routing, but the scorecard counts them as unassigned and the README claims every open item is assigned.
scripts/maintainer/metrics.ts#L238-L246: excludesuspectedSpamPRs from the assignment-coverage denominator.scripts/maintainer/README.md#L5-L9: describe assignment as applying only to eligible items and note cap/flag exceptions.
π Affects 2 files
scripts/maintainer/metrics.ts#L238-L246(this comment)scripts/maintainer/README.md#L5-L9
π€ Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@scripts/maintainer/metrics.ts` around lines 238 - 246, Update the
assignment-coverage calculation around assignablePRs and pctPRsAssigned to
exclude PRs marked suspectedSpam from the denominator, while preserving the
existing draft exclusion and assigned-count logic. In
scripts/maintainer/README.md lines 5-9, clarify that assignment applies only to
eligible items and document the cap/flag exceptions.
π― Changes
Adds a maintainer toolset β two scheduled workflows backed by a unit-tested TypeScript toolkit in
scripts/maintainer/, configured via.github/maintainers.json:maintainer-sweep.yml, every 3h): assigns open PRs/issues to maintainers by least-loaded rotation with caps, posts a one-time ack comment with a pre-review checklist (CI, conflicts, changeset, E2E), asks for a reproduction on bug reports lacking one, and keepswaiting-on/ready-to-merge/needs-repro/has-prlabels in sync. Suspected drive-by/bounty PRs are flagged for human judgment, never acted on.maintainer-scorecard.yml): posts a to-do digest β SLA breaches, ready-to-merge PRs, per-maintainer queues, new/stale items, unanswered discussions, and response-time stats. Untrusted item titles are markdown-escaped before rendering.Everything is deterministic and stateless: metrics are recomputed from GitHub timelines each run, PR code is never checked out or executed (no
pull_request_target), and workflows run with minimal permissions and pinned action SHAs. Both workflows support manual dry-run dispatch.AI-assisted triage (issue summaries, smarter repro detection, duplicate detection) is intentionally not in this PR β it will come as a follow-up layered on top of this deterministic base.
Before enabling: create the
DISCORD_MAINTAINER_WEBHOOKsecret and add Discord user IDs tomaintainers.json. Seescripts/maintainer/README.md.β Checklist
pnpm run test:pr.π Release Impact
π€ Generated with Claude Code
Summary by CodeRabbit
New Features
Documentation
Tests