Appearance
label-check.mjs
Reads every open ticket on the Forge and refuses a tracker whose triage labels have drifted.
Agent tooling › label-check.mjs
What it does
Asks tools/forge.mjs for every open issue and holds three invariants over each one: it carries exactly one triage label, that label is not a retired one, and it is not unlabeled. The triage axis holds a single value, so two labels from it on one ticket is a breach rather than a refinement of it.
The axis is ready-for-agent, ready-for-human and blocked. The labels it names as retired -- backlog, needs-triage, needs-info -- were folded into that axis and must not come back. A label outside the axis is counted in the distribution and never judged: a ticket may carry grill or wontfix alongside its triage label without that being a breach.
The CI log mailbox is exempt. It is a drop rather than a ticket, so it carries no triage label and the invariant does not reach it.
This is deliberately not a leg of npm run check. It reads the Forge over the network and the gate is offline, so wiring it in would make the gate fail on a dropped connection. Run it by hand before and after a label move instead, so a sweep has a repair list the way a code sweep has the gate.
Usage
Read the whole open tracker and report every breach:
bash
node tools/label-check.mjsOptions
It takes no flags. What it checks is fixed, because a check whose strictness is an argument records whatever the caller was willing to pass.
Inputs and outputs
Reads open issues through node tools/forge.mjs list-issues --state open --limit 200 --json. Writes nothing, and mutates no label: a check that repaired what it found could not be run to learn the starting state.
The breach kinds it can print:
| Kind | What it means | The usual repair |
|---|---|---|
retired | The ticket carries a label the axis no longer has. | Add the survivor it maps to, then remove the retired one. |
multiple | Two triage labels on one ticket. | Decide which state the ticket is actually in and drop the other. |
unlabeled | The ticket carries no labels at all. | Triage it. This is the only kind that needs a judgement rather than a move. |
none | It carries labels, but none from the triage axis. | Add the triage label. The other labels stay. |
Prints the open count, then the distribution one line per label, with a RETIRED marker against any that should no longer exist. Each breach follows as breach <kind>: <n> -- <ids>, listing at most eight ids and marking the rest with an ellipsis. Exit 0 on a clean tracker with one closing line; exit 1 with the breach count when anything is wrong.
Examples
Take the reading before a label sweep, so the starting state is recorded rather than remembered:
bash
node tools/label-check.mjsRun it again afterwards. The breaches it prints are the repair list, and each one is repaired forward from the invariant rather than from the label's earlier meaning.
Delete a retired definition only once nothing carries it:
bash
node tools/forge.mjs delete-label --label backlogNotes
The rules live in auditLabels, which is pure and takes the issues as an argument, so test/tools/label-check.test.mjs exercises every breach kind without reaching a Forge.
A label that is still carried cannot be deleted. delete-label refuses while any open issue holds it and names the first few, and it reports how many closed issues will lose the label when it goes. Unlabel first, prove it with this check, then delete the definition.
A tracker mutation has no gate behind it the way a code change does. Nothing turns red when a ticket silently loses its only label. So the reading is taken before and after a sweep, rather than trusting the sweep's own report of what it did.
Source
tools/label-check.mjs — part of Agent tooling