chore(repo): add issue and PR triage tooling, and a batch PR reviewer - #37082
Open
AgentEnder wants to merge 17 commits into
Open
AgentEnder wants to merge 17 commits into
AgentEnder wants to merge 17 commits into
Conversation
Triage produces label changes, assignees and public comments. Applying those straight from an agent means the first time a human sees a mistake, it is already on a public issue and cannot be unsent. This stages each mutation instead, and only a human moves it forward. - `.claude/tools/triage` writes one hand-editable markdown record per issue and applies approved ones through gh. It refuses to stage the bot-owned `stale`/`outdated` labels, two stale countdowns at once, a label both added and removed, or a close with no comment explaining it. - `.claude/tools/triage-tui` is the approval surface: approve, reject, leave a note the agent reads back with `triage feedback`, open the record in $EDITOR, or open the issue on GitHub. - `triage-owners.json` routes scopes to maintainers and rotates rows with several owners, so one person does not absorb a whole sweep. The TUI is its own workspace package because ink depends on yoga-layout, which uses top-level await and cannot be transformed to CommonJS; the sibling package.json scopes just that directory to ESM. It also keeps ink out of the root dependency tree. `pnpm triage-tui` runs it. Everything except the TUI runs on node builtins alone, so applying still works in a fresh clone before pnpm install.
A repo-facing workflow for triaging nrwl/nx issues: apply the labels that make an issue count as triaged, ask for a reproduction when one is missing, run it when one exists, and propose a priority and an owner. Every mutation is staged through .claude/tools/triage for a human to approve; the skill never writes to GitHub itself. The definition of "triaged" is not a convention here. Any open issue with no `scope:` label increments untriagedIssueCount in scripts/issues-scraper/scrape-issues.ts, which issue-notifier.yml posts to Slack weekly, so the queue query mirrors that scraper exactly. Things the skill encodes because they are easy to get wrong: - A `blocked:` label is an eviction timer, not a note. schedule-stale.yml gives 7 days to `stale` and 21 more to closed. Removing one when the reporter answers is a triage duty, since the bot only strips `stale`. - `stale` and `outdated` are bot-owned and must never be set by hand. - closedByPullRequestsReferences finds a PR already in flight, including on open issues. Scraping /timeline for cross-referenced events does not. - The scope table is built from how maintainers actually label, not from the label descriptions: `scope: bundlers` reads "webpack, rollup" but covers vite, esbuild, rspack and rsbuild too. - A `Steps to Reproduce` section containing only `1.` is the bug form's seeded default, not a reproduction. No labels at all means the issue was filed outside the form, so none of its required fields are guaranteed. - Before proposing `community`, check the fix is contributable. The repo does ship graph UI source under graph/, but those packages import the published closed-source @nx/graph, so "it's a graph bug" decides nothing. references/labels.md holds the scope map and close reasons; references/replies.md holds comment templates.
A reproduction clones a stranger's repo and installs it with that repo's own package manager. Fetching that into the shared object store would mix an untrusted history into the repo every PR review reads. - `sandbox start` without `--checkout` now provisions an empty private directory in the shared host instead of a worktree of nx, with `root` pointing at the workspace itself since there is no `nx` subtree. - `sandbox exec` skips `ensureInstalled` for a scratch workspace: there is nothing to install, and `mise install && pnpm install` fails in an empty directory. - reproduce-issue is rewritten onto that mode, so repro code runs in the shared host rather than a container of its own.
Nothing reaches GitHub until a human approves it. Every label, assignee, comment and close is written to `.nx-issue-triage/<N>.md` first, and only `apply` calls `gh`. - Add the record lifecycle: stage, list, show, set, feedback, apply, rm, prune, and `notify` for pushing into a live TUI. - Refuse to stage a record that sets a bot-owned label, starts two stale countdowns at once, adds and removes the same label, or closes an issue with no comment, so the guardrails hold when the skill is only skimmed. - Journal every write to ~/.nx-triage/journal.jsonl, outside the state directory, so `recover` can restore records if that directory is cleared. It is gitignored and shared between sessions, so it really does get lost. - Record the issue's current labels and assignees at stage time, so review can tell a real mutation from a no-op restatement. - Move owner routing from triage-owners.json to a documented triage-owners.md. The table now carries a roster that `owner` asserts against, so a handle that has left the team fails loudly instead of quietly drawing issues, plus freeform per-area guidance and per-person standing notes that JSON had nowhere to put.
Reviewing staged records by reading markdown and running `triage set` by hand does not scale past a handful, and a sweep stages dozens. - Approve, reject, request changes with a note, open the issue on GitHub, and edit a record in $EDITOR from the list. - Reload as the state directory changes, so records land in front of the reviewer while the agent is still staging them. - Accept pushes from `triage notify`: reload, select a record, or show a message, so the agent can point the reviewer at what it just restaged. - Render the record body rather than the raw file, and keep comment text selectable without picking up soft-wrap breaks.
The skill's failure mode was a confident record built on reading rather than running. Every excuse in the new table was written into a real record here, sounded reasonable, and was wrong. - Fetch the whole queue in one GraphQL call instead of three REST calls per issue, and read each issue out of the file rather than into context. - Default to attempting a reproduction. `repro: not run` is legitimate in exactly two cases, an open PR or nothing runnable, and must name which in its first sentence. audit-repro.sh checks that mechanically. - Add a Step 3 check for a fix that landed WITHOUT a closing keyword: #36426 was fixed by a PR that said `Fixes` on its later duplicate, so every PR-detection path came back clean on an issue already fixed. - Require plain prose outside the collapsed Triage notes block on every comment. A bare `<details>` reads as a bot dropping an artifact on the thread, which is what the attribution split exists to prevent. - Note that a Windows VM is reachable, so `os: windows` is not unrunnable, and that the sandbox is Linux, so a clean run narrows a darwin bug rather than clearing it.
The agent and the reviewer work in parallel: records land while the reviewer is still deciding on earlier ones. That needs the TUI beside the conversation, not in front of it, and it needs decisions coming back without the agent polling. - Open `triage review` in a herdr or tmux split, self-closing on quit. - Arm watch-triage.sh as the return path: one line per status change with the reviewer's note, exiting once every record settles. It also reports if the state directory disappears mid-review, which a stray clean elsewhere in a shared checkout really can do. - Say plainly that this needs a multiplexer rather than degrading to a foreground TUI that takes the session over, or a detached one that exits immediately for want of a terminal. - Apply approvals as they arrive rather than at the end, so a reviewer who approves ten and steps away comes back to ten applied.
Nx bans the loudest machine-written tells already, but only inside astro-docs: STYLE_GUIDE.md forbids em dashes, en dashes and semicolons, and the vale styles list 60-odd banned phrases. vale runs on astro-docs/** only, so none of it covers the text agents actually send outward: PR comments, issue replies, review comments, triage comments. Adds the skill plus scripts/deslop-scan.mjs, which applies those same committed rules to any file or to stdin. Landing it first so tools/triage can delegate its comment check to the scanner instead of carrying a regex of its own.
Drafts at $REVIEW_DIR/<PR>.md were already markdown with frontmatter, but they carried no lifecycle. Every consumer re-derived state from ls plus grep '^verdict:' plus mtime, and each caller invented its own freshness window, which is what made a previous round's draft read as a new one. Adds .claude/tools/review as the one record store, with a status: key that runs from awaiting-grill through posted or discarded, and a watch verb: one long-lived process emits a line per transition, so a batch needs no watcher of its own. review-pr and review-pending-pr-reviews now write status: alongside posted_at, since the two answer different questions. review-pr's Step 8 rewrites the whole file, so it carries an explicit warning that dropping the key makes an in-flight review look finished. review-many-prs is the first consumer: it fans review-pr out over several PRs in their own herdr tabs, three at a time, holding each slot until the child parks at the grill.
Triaging a PR and triaging an issue differ in what they decide, but not in how an owner is chosen or how a mutation is staged for approval. triage-issues carried all of it inline, so adding triage-prs meant either a second copy of the common half or a skill that reimplements it from memory. Extracts that half into .claude/skills/triage-shared: owners.md for the routing rules and staging.md for the record format. It has no SKILL.md on purpose, since it is reference material rather than a procedure. Both skills point at it, so a correction lands once rather than in one of two copies. Adds triage-prs, which decides who handles a PR, drafts it when CI is red, checks for a linked GitHub or Linear issue, and judges whether an unlinked community PR should stay open. tools/triage grows the PR-side verbs and hands its comment check to the deslop scanner rather than a regex kept here. The TUI becomes the approval surface for both record kinds.
The review TUI had one way to say something back: c, which sets the record to changes-requested and holds the apply until it is restaged. That is the right channel for "this record is wrong" and the wrong one for "and also open a Linear ticket for this one", which decides nothing about the staged mutations and should not gate them. Adds n. It writes to the events bus rather than into the record, so the status is untouched, an approved record can carry a note without delaying its apply, and the note outlives apply archiving the record it names. triage note and triage notes are the CLI ends of the same channel. The detail pane shows a record's notes back, because a note is otherwise write-only: the flash confirming it is gone in four seconds, and a reviewer who cannot see what they already asked for writes it twice. That bus turned out not to be trustworthy enough to carry the only copy of anything. appendJsonl swallowed every write error, so an unwritable state dir looked exactly like a quiet queue; it now records the failure, warns once from the CLI, and the TUI says the note was not saved rather than claiming it landed. Three paths also changed the review list while emitting nothing: e hands the record to $EDITOR, recover restores one from the journal, and rm and prune take one out. Each emits now, so the bus is a complete account of what happened. A rejection archives itself the moment it is made, the way apply retires a record it has already written to GitHub. Nothing downstream acts on one, so leaving it in the list only made the reviewer scroll past their own decisions. That last change is what retires watch-triage.sh. It reported by diffing directory snapshots, which can only see records still present at the end of a tick, so anything archived produced no line at all: approved to applied to archived passed in silence, and its applied case was dead code. triage monitor reads the bus instead, where the transition is recorded before the file moves. It seeds the pending count once from disk, since absolute state is the one question events cannot answer alone, and keeps it from the bus after that.
Five files under .claude were failing the format check before any of this
branch's work, so prepush was already red for reasons nobody was going to connect
to the change in front of them.
Formatting only, no content changes. oxfmt reports the whole changeset clean.
tools/triage is not included because neither formatter covers it. It is an
executable Node script with a shebang and no extension, so oxfmt matches no
target file for it and nx format:check skips it for the same reason. Worth
knowing before reaching for prettier directly to fill that gap: prettier resolves
neither a config file nor the .editorconfig section keyed to
[*.{kts,kt,js,ts,jsx,tsx}], falls back to its own defaults, and rewrites every
string literal in the file from single to double quotes.
…he fixups off The pane said "stale" for any head that moved, which was wrong twice over. An empty [Self-Healing CI Rerun] commit changed nothing and still read as stale, and a rebase that preserved the PR's change read the same as one that rewrote it. Flagging both trains you to ignore the flag. Comparing shas only says the head moved, and comparing trees cannot answer the rebase case, because a rebase changes the tree even when the PR's own change is identical. Comparing the patch-id of the PR's diff against its merge base does. The +/- count is restricted to the files the PR touches at either point; a whole-tree diff across a rebase reports the upstream churn the branch landed on, which on #36912 was 1354 insertions of other people's work against 417 of the author's. All of it is local git, so it costs no gh approval. The detail pane also lost the last two lines of every long record, because a scrollbox's height counts its borders and it was being handed two more rows than it could draw. It now has an explicit height, renders the whole record rather than the current attempt's section, and scrolls from the keyboard. `c` copies a handoff prompt carrying the branch, the draft's path and the drift, so a fixup session starts from the record instead of rediscovering it.
Both files drifted again after the earlier formatting pass, so prepush would have gone red on work unrelated to whatever change was in front of the person pushing. Formatting only. oxfmt reports the whole of .claude clean afterwards, and review.spec.ts still passes.
The driver needs herdr to create tabs for its children. It does not need one for itself, and giving it one spends a tab that someone then has to close by hand. Backgrounding it with its output on disk costs nothing and lets the harness report the exit. Also records two things the skill was silent on: drafts are skipped unless --include-drafts, and --dry-run belongs in the foreground.
The TUI runs under Bun because opentui reaches the terminal through Bun's FFI, and nothing in the skills said so. `triage review` already checks for bun and prints the install line, so the tool was the only place that knew. The workspace comment still claimed the package existed to keep ink out of the root dependency tree. Ink was replaced by opentui several commits ago, so the comment named a dependency that is no longer there.
Adding a workspace package re-resolves every dependency name it shares with the rest of the repo, and this one shared two. `@opentui/react: ^0.5.9` floated to 0.5.11 while `@opentui/core` was pinned to 0.5.9, so the lock carried two copies of a package that ships a prebuilt binary for seven platforms. react@0.5.9 depends on core@0.5.9 exactly, so pinning both leaves one. `ws` was declared only to satisfy an @opentui/react peer that nothing in the TUI imports, and every version of it perturbed somebody else: `^8.18.0` pulled five consumers up to 8.21.1, `8.21.1` pulled eight, `8.19.0` pulled four down. Dropping it lets pnpm satisfy the peer from the tree that is already there. `pnpm install --lockfile-only` now adds 175 lines and deletes none. Verified against a control run with the workspace entry removed, which produces no lock churn at all, and by rendering the real TUI under a pty against the trimmed dependency set.
✅ Deploy Preview for nx-docs ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
✅ Deploy Preview for nx-dev ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
Contributor
|
View your CI Pipeline Execution ↗ for commit ea260f0
☁️ Nx Cloud last updated this comment at |
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Current Behavior
Triage is manual, and a wrong label or a clumsy comment is public the moment it is made. Reviewing several PRs means running
/review-prone at a time.Expected Behavior
.claude/tools/triagestages label changes, assignees and comments as markdown records under a gitignored directory.pnpm triage-tuiis where they get approved, rejected, or sent back with a note. Nothing reachesghbefore that./triage-issuesapplies scope and type labels, asks for a reproduction when one is missing, runs it when one exists, and proposes a priority and an owner from.claude/tools/triage-owners.md./triage-prspicks an assignee, drafts a PR whose CI is red, and checks for a linked issue. Neither one is a code review./review-many-prsfans/review-prout over several PRs, three at a time, each in its own herdr tab..claude/tools/reviewgives the drafts a status so a batch does not need its own watcher.Related Issue(s)
Fixes #