docs: rules for issue triage and board hygiene - #1932
Open
cliffhall wants to merge 4 commits into
Open
Conversation
AGENTS.md had no rule linking a milestone to a board Status, and its two existing rules contradicted each other: every new issue was to be milestoned at create time, yet new items were to start in Incoming, with Todo reserved for work a maintainer had approved. If every issue is milestoned at birth, "has a milestone" carries no approval information and nothing records the moment of sign-off. Make assigning a milestone the approval act, giving one invariant: Incoming <=> no milestone; everything past it <=> milestoned. External reports and maintainer-filed issues then differ only in whether approval has already happened -- an outside reporter cannot set a label, milestone, or board field, so their issue arriving with none of them is normal rather than a defect to fix on arrival. - scope the "not created until five things" callout to maintainer-filed issues - split the Status bullets into the two cases - scope the milestone mandate to issues you create, stating the external exception explicitly - add a "Triaging externally-filed issues" section: the two passes, plus a gh/jq snippet that diffs open issues against the board to find unboarded ones - reword both prose descriptions of the Incoming/Todo line - reword the rubric's "Linked to a milestone" bonus as "already approved" and note it is the re-scoring case only; under the old rules it was a constant offset on every issue, shifting every band boundary down by one - update both gh recipes (#28 and #11) to default to Todo, with an Incoming variant for triage No mirror to .github/copilot-instructions.md: board and milestone mechanics are on that file's deliberately-absent list. Closes #1930 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YAt8rqxysNbhYWLhoRm3fU
…d audit Follow-up on the same issue: the triage rules were clear on the happy path but three gaps meant a bare "triage new issues" would diverge from what a maintainer actually does. Define triage by STATE, not authorship. An issue needs triage when it arrives with no board card and no milestone — whoever filed it. Framing it as "externally-filed" was wrong: a maintainer opening an issue by hand in the GitHub UI lands in exactly the same state, because write access makes the board reachable, not automatic. What puts an issue in Todo is somebody performing the approval, not who owns the account. Renamed the section and reworded every dependent passage accordingly. Fix the discovery query, which diffed open issues against board #28 only. Verified against the live boards: it reports #1929 as unboarded when that issue is correctly carded on #11, so following it literally double-boards the issue — recreating a defect a past sweep introduced. Now diffs against the union of both boards, filtered by .content.repository (an org project can hold other repos' issues; #11 carries one from modelcontextprotocol/ servers), and prints each hit's destination, since an unboarded issue that already has a milestone is approved and belongs in Todo, not Incoming. Add "The board audit" — the other drift classes a single-issue rule can't catch (double-boarded, non-Issue items, statusless cards, the Incoming ⇔ milestone invariant in both directions, wrong board for label, missing version label, missing Priority), as a table of invariant + fix plus one runnable jq check that should print 0 across the board. Both snippets were executed verbatim against the live boards. Add "Recording the score". The rubric claimed its reasoning "survives in a form someone can argue with later", but the board stores only the result and is private, so nothing kept that promise. Triage now posts the axes, the bonuses claimed, and the total as a comment. Also scope pass 2 explicitly to a human — deciding what ships in which release is not inferable from a rubric — and correct the stale note about three statusless cards on #11. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YAt8rqxysNbhYWLhoRm3fU
…r way A board card in Done asserts that the work shipped, and Done is read as the record of what a milestone actually delivered. Nothing said so, so an issue closed as a duplicate or not-planned was being moved to Done like any other close — which makes that record wrong in a way no later reader can detect: counting Done cards can no longer tell a shipped fix from a report closed as a duplicate of one. New rule: exactly two things earn a card a place in Done — its PR merged, or it is a parent whose last sub-issue closed. Every other close (duplicate, won't fix, not planned, obsolete, superseded) means nothing shipped, so the card is deleted instead. Deleting a card touches only the board; the issue keeps its labels and comments and stays searchable, so nothing is lost. Also notes that the close reason is the machine-readable form of the same distinction, and that `gh issue close --reason` cannot express `duplicate` — it accepts only completed/not planned, so duplicate must be set through the API or the web UI's "Mark as duplicate" (which also records a duplicate-of link). Adds the invariant to the board audit as a tenth check. That required the audit to read closed issues too, so its fetch moves to --state all with stateReason, `open(n)` becomes a state test rather than "present in the map", and --limit must now clear the repo's total issue count, not just the open ones. The two wrong-board label checks are scoped to open issues in the same pass — over all history they flag four long-closed cards whose labels nobody intends to rewrite. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YAt8rqxysNbhYWLhoRm3fU
…ion) The version label says which line work belongs to; nothing said what KIND of work it is. In practice that meant reaching for bug or enhancement and pressing everything else into one of them — a migration guide and a tsup->tsdown swap both landing as "enhancement", which degrades the label to "not a bug" and makes filtering by it meaningless. Every issue created or triaged now carries exactly one of bug, enhancement, documentation, chore, or question, independent of its v1/v2 label. Adds a table of what each is and is not for, and states the anti-pattern explicitly so the two familiar labels don't absorb the other three. A PR needs no type label — it is classified through the issue it closes, the same way it is tracked through that issue's board card. Wires the requirement into the three places that would otherwise drift: the "not created until" callout, triage pass 1 (an outside reporter can set neither label, so both are applied in the same step), and the board audit, which gains an "open, no type label" check and a matching row in the invariant table. Note `chore` did not exist as a repo label and was created for this; the existing `dependencies` label is Dependabot's and scoped to PRs. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YAt8rqxysNbhYWLhoRm3fU
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.
Closes #1930
AGENTS.md's issue rules had a contradiction, several gaps, and nothing checkable. This makes them coherent and adds a runnable audit so they stay that way. Documentation only — one file, no code.1. A milestone is what records approval
Two rules disagreed: every new issue gets a milestone at create time, and new items start in
Incoming— whereIncomingmeans "unreviewed" andTodomeans "a maintainer approved it." If every issue is milestoned at birth,Incomingno longer distinguishes reviewed from unreviewed work, and the priority rubric's "linked to a milestone" +1 becomes a constant every issue earns.Assigning a milestone is now the approval act, which gives one invariant:
2. Triage is defined by state, not authorship
The tempting framing is "externally-filed issues." That's the wrong line — an outside reporter has no board access, and neither does a maintainer opening an issue by hand in the GitHub UI. Write access makes the board reachable, not automatic.
A permission-probe step (
gh api …/collaborators/<login>/permission) was drafted and then removed: routing on authorship would send a maintainer's hand-filed issue toTodowithout anyone having approved it.3. Triage has to be runnable
.content.repository(board get tools working #11 carries amodelcontextprotocol/serversissue, which an unfiltered check reads as a defect).Todo, or triage would silently un-approve scheduled work.4. The board audit
Only one drift class ("no card at all") was written down. Added a table of invariant + fix for the rest — double-boarded, PRs as board items, statusless cards, both directions of the
Incoming⇔ milestone invariant, wrong board for the version label, missing version/type labels, missing Priority, and closed-but-never-shipped — plus one runnablejqcheck that should print0on every row.5.
Donemeans the work shippedNothing said what
Doneasserts, so an issue closed as a duplicate was moved there like any other close. That makesDone— read as the record of what a milestone delivered — wrong in a way no later reader can detect.Exactly two things earn a card a place in
Done: its PR merged, or it is a parent whose last sub-issue closed. Every other close deletes the card. Deleting a card touches only the board; the issue keeps its labels and comments and stays searchable.Also documents that
gh issue close --reasonaccepts onlycompleted/not planned, soduplicatemust be set through the API or the web UI's "Mark as duplicate."6. Every issue carries a type label
The version label says which line; nothing said what kind. In practice everything got pressed into
bugorenhancement— a migration guide and atsup→tsdownswap both landing as "enhancement," which degrades the label to "not a bug."Every issue created or triaged now carries exactly one of
bug/enhancement/documentation/chore/question, with a table of what each is and is not for. A PR needs no type label — it is classified through the issue it closes.Validation
Every query in this PR was executed verbatim against the live boards, not just eyeballed — and each rule caught real defects as it landed:
Donethat never shippedv1issue carded on board Add tab and approval flow for server -> client sampling #28 while holding milestone v2.5.0, a v2 release bucketTodoissues with no type labelIn Progresswith no milestone (left for a maintainer; it is a pass-2 call)Notes
npm run cigates nothing here. It passed in full on the first commit..github/copilot-instructions.md. Board, milestone, and triage mechanics are on that file's deliberately-absent list — no reviewer cites them against a diff.AGENTS.mdis not covered by anyformat:checkglob and was already prettier-dirty onv2/mainbefore this branch (verified by stashing), so no reformatting is included — that would be unrelated churn.🤖 Generated with Claude Code
https://claude.ai/code/session_01YAt8rqxysNbhYWLhoRm3fU