Skip to content

docs: rules for issue triage and board hygiene - #1932

Open
cliffhall wants to merge 4 commits into
v2/mainfrom
v2/docs/incoming-todo-milestone-rule
Open

docs: rules for issue triage and board hygiene#1932
cliffhall wants to merge 4 commits into
v2/mainfrom
v2/docs/incoming-todo-milestone-rule

Conversation

@cliffhall

@cliffhall cliffhall commented Aug 5, 2026

Copy link
Copy Markdown
Member

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 — where Incoming means "unreviewed" and Todo means "a maintainer approved it." If every issue is milestoned at birth, Incoming no 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:

Incoming ⇔ no milestone. Everything past it ⇔ milestoned.

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.

An issue needs triage when it arrives with no board card and no milestone — regardless of who filed it.

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 to Todo without anyone having approved it.

Arrives unboarded Created through the documented flow
Arrives with no label, no milestone, no card label + type + milestone + card
Triage pass 1 label + type + board + Priority + Incoming, no milestone
Approval pass 2: maintainer assigns milestone → Todo implicit in creating it

3. Triage has to be runnable

  • Corrected discovery query. The obvious version diffs open issues against board Add tab and approval flow for server -> client sampling #28 only — verified against the live boards, it reports Track the 5 open v1 security PRs (board #11 issues-only cleanup) #1929 as unboarded when that issue is correctly carded on get tools working #11, so following it double-boards the issue. Now diffs the union of both boards, filtered by .content.repository (board get tools working #11 carries a modelcontextprotocol/servers issue, which an unfiltered check reads as a defect).
  • Two passes, explicitly separated. Pass 1 sweeps issues onto the board and implies no approval. Pass 2 — what ships in which release — is reserved for a human and never done as part of a routine sweep.
  • The milestoned exception. An unboarded issue that already has a milestone was approved and only lost its card; it goes straight to Todo, or triage would silently un-approve scheduled work.
  • Recording the score. The rubric promised its reasoning "survives in a form someone can argue with later," but the board stores only the result and is private. Triage now posts the axes, the bonuses claimed, and the total as an issue comment.

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 runnable jq check that should print 0 on every row.

5. Done means the work shipped

Nothing said what Done asserts, so an issue closed as a duplicate was moved there like any other close. That makes Done — 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 --reason accepts only completed/not planned, so duplicate must 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 bug or enhancement — a migration guide and a tsuptsdown swap 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.

chore did not exist as a repo label and was created for this. The existing dependencies label is Dependabot's and scoped to PRs.

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:

Notes

  • Docs-only — no code changes, so no screenshots apply, and npm run ci gates nothing here. It passed in full on the first commit.
  • No mirror to .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.md is not covered by any format:check glob and was already prettier-dirty on v2/main before 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

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
cliffhall and others added 3 commits August 5, 2026 15:40
…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
@cliffhall cliffhall changed the title docs: make assigning a milestone the act that moves a card to Todo docs: rules for issue triage and board hygiene Aug 5, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

v2 Issues and PRs for v2

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant