chore(repo): add the issue-triage tooling - #36883
Conversation
✅ 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. |
|
View your CI Pipeline Execution ↗ for commit 7ed7417
☁️ Nx Cloud last updated this comment at |
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
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.
Co-authored-by: AgentEnder <AgentEnder@users.noreply.github.com>
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.
30c281d to
4bc1615
Compare
An arm that quietly exercises the absence of the feature under test comes back clean, and reads as a negative result. - `CLAUDECODE=1` makes `isAiAgent()` true, and `shouldUseTui()` returns false on it, so an agent-run TUI test is never on the TUI path. - A pty with no winsize refuses the TUI outright via the `columns > 0` check, which is how a hook runner's inner pty behaves. - Assert the state under test by calling the predicate or finding a byte-level tell, rather than inferring it from the command line. #36579 was measured twice on the non-TUI path before anyone checked.
There was a problem hiding this comment.
Important
At least one additional CI pipeline execution has run since the conclusion below was written and it may no longer be applicable.
Nx Cloud is proposing a fix for your failed CI:
We ran pnpm nx format to resolve the Prettier formatting check failure on .claude/skills/triage-issues/SKILL.md, a new file introduced by this PR that was committed without being passed through the formatter. This brings the file into compliance with the repo's formatting standards and will allow the nx format:check task to pass.
Tip
✅ We verified this fix by re-running nx-cloud record -- nx format:check.
diff --git a/.claude/skills/triage-issues/SKILL.md b/.claude/skills/triage-issues/SKILL.md
index a412bf6e4..d57c8f5a5 100644
--- a/.claude/skills/triage-issues/SKILL.md
+++ b/.claude/skills/triage-issues/SKILL.md
@@ -381,11 +381,11 @@ problem, and check it against the flags you are about to set.
**Your own environment is a flag you did not set.** `CI` is the one you choose; these are set for you,
and each one silently reroutes the code under test:
-| Set by | What it changes |
-| --- | --- |
-| `CLAUDECODE=1`, and the other `CLAUDE*` vars | `isAiAgent()` returns true, and `shouldUseTui()` returns false on it. Every TUI test an agent runs is on the non-TUI path unless the vars are stripped. |
-| The pty you run under | `is-tui-enabled.ts` gates on `process.stdout.columns > 0 && rows > 0`. A pty with no winsize (lefthook's inner pty reports 0x0) refuses the TUI outright. |
-| `SANDBOX_RUNTIME=1` | `isSandbox()` is true inside the review sandbox, which some paths branch on. |
+| Set by | What it changes |
+| -------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `CLAUDECODE=1`, and the other `CLAUDE*` vars | `isAiAgent()` returns true, and `shouldUseTui()` returns false on it. Every TUI test an agent runs is on the non-TUI path unless the vars are stripped. |
+| The pty you run under | `is-tui-enabled.ts` gates on `process.stdout.columns > 0 && rows > 0`. A pty with no winsize (lefthook's inner pty reports 0x0) refuses the TUI outright. |
+| `SANDBOX_RUNTIME=1` | `isSandbox()` is true inside the review sandbox, which some paths branch on. |
The failure is always the same shape: the arm you believe is exercising the feature is quietly
exercising its absence, and it comes back clean. **Assert the state you are testing rather than
🔔 Heads up, your workspace has pending recommendations ↗ to auto-apply fixes for similar failures.
Or Apply changes locally with:
npx nx-cloud apply-locally zoMV-g4iE
Apply fix locally with your editor ↗ View interactive diff ↗
🎓 Learn more about Self-Healing CI on nx.dev
Reproducing #36520 needed 160 unattended runs per version, and each of these cost an iteration before it was understood. - `tui.autoExit: 0` is a duration, so it means never auto-exit, not exit immediately. A loop set that way waits forever for a keypress. - 22.7.x keeps the TUI open after a task fails and `--tuiAutoExit=true` does not override it, so an unattended run hangs on exactly the case being measured. Feed it a quit. - `--tui` and `--outputStyle` are mutually exclusive; passing both prints help and exits 1, which reads as the reproduction failing. Also: size the pty, or the capability check refuses the TUI and the run measures the non-TUI path again.
7959e71 to
7ed7417
Compare
Current Behavior
Triaging issues in this repo is unscripted, and the one step that actually counts is easy to miss:
scripts/issues-scraper/scrape-issues.tscounts any open issue with noscope:label as untriaged, and.github/workflows/issue-notifier.ymlposts that number to Slack every Sunday.There is also no way to look over a batch of proposed label, assignee and comment changes before they hit
GitHub — you either run
gh issue editand live with it, or you don't triage.Expected Behavior
Adds the issue-triage tooling under
.claude/. Nothing reaches GitHub until a human approves it..claude/tools/triage— stages every mutation as a reviewable record under.nx-issue-triage/rather than calling
ghdirectly:stage,list,show,review,feedback,apply, plusapply --dry-runto print the exactghcommands without running them. Records are plain markdown andhand-editable. Every write is journalled to
~/.nx-triage/journal.jsonl, outside the state directory,so
recovercan restore records if that directory is cleared..claude/tools/triage-tui— the approval TUI (pnpm triage-tui): approve, reject, request changeswith a note back to the agent, open the issue on GitHub, or edit the record in
$EDITOR..claude/tools/triage-owners.json— area routing. Weighted round-robin over per-area candidatesplus freeform guidance, so a sweep spreads across the team instead of clustering on one person, and a
routing decision someone had to reason out gets recorded rather than re-derived next time.
.claude/skills/triage-issues/— the workflow itself, with label and reply references.The guardrails are enforced by the tool rather than only documented. It refuses to stage a record that
sets the bot-owned
staleoroutdatedlabels, that starts two stale countdowns at once, that adds andremoves the same label, or that closes an issue without a comment.
Opening as a draft: this is the initial version, and further refinements to the tool, the TUI and the
skill are still local.
Related Issue(s)
None — repo tooling, no linked issue.