Issue Taxonomy and the Release Readiness Gate
A queue you cannot route is a queue you cannot finish. Before this taxonomy an open issue carried a type at best, so the two questions that actually drive work — what is still open for this plugin? and what is still open for this feature? — could only be answered by reading every title. And the question that gates a release — is anything blocking? — could not be answered at all.
Every open issue now carries up to four labels (closed issues are out of scope — policy
issue-taxonomy-scope, see the last section). Three are routing. One is a gate.
The four axes
| axis | label | applies to | purpose |
|---|---|---|---|
| Type | bug · enhancement · documentation · chore |
everything, exactly one | what kind of work |
| Area (core) | area:<subsystem> |
MeshWeaver | which part of the framework |
| Plugin (modules) | plugin:<Partition> |
plugin repos | which module owns it |
| Feature | feature:<slug> |
everything with a specific subject | the thing itself, across repos |
| Severity | sev:B · sev:H · sev:M · sev:L |
bug only |
how bad |
chore means CI, build, dependencies or cleanup with no user-visible behaviour change.
feature: is the axis that crosses repositories. A defect in core and the plugin change that
depends on it share a feature slug and nothing else; that shared slug is the only way to see them
as one piece of work. It also exposes duplication that titles hide — the first pass found six
separately-filed issues that were one RoutingGrain back-pressure defect, one per activation.
Severity, and why only bugs have it
| label | meaning |
|---|---|
sev:B |
BLOCKING — the release cannot ship while this is open |
sev:H |
BLOCKING — a primary path broken but a workaround exists; an intermittent user-visible failure; or silently WRONG results anywhere |
sev:M |
a secondary path broken, or a clear defect with an easy workaround |
sev:L |
cosmetic, a rare edge case, or developer-only |
An enhancement, documentation or chore has no severity. If something is not broken it
cannot block a release, and letting a feature request carry a blocking flag is how a release gate
rots into a wish-list.
sev:B and sev:H are the classes where "we will do it next sprint" is not an available answer
— policy release-blocker-gate. sev:M and sev:L are a priority conversation and
never gate a cut.
The gate
label:bug label:sev:B state:open → must be ZERO
label:bug label:sev:H state:open → must be ZERO
across the seven repositories that carry the taxonomy: MeshWeaver, MeshWeaver.Plugins,
MeshWeaver.Crm, MeshWeaver.SocialMedia, MeshWeaver.Reinsurance, MeshWeaver.Manufacturing,
MeshWeaver.Education. Memex and MeshWeaver.Feedback are deliberately not gated: they ship no
product code.
🚨 Name the repositories; never write "every repo of the product". A repo that is not on the list is not gated, and — worse — a repo on the list that has never had the
sev:Blabel created answers a label query with an empty array, which folds to "no open issues" and is green forever. That was live: the gate named seven repositories and the label existed in five — which is whyNoOpenIssuesnow refuses a label that does not exist instead of folding it to Green (MeshWeaver.Plugins#2190).
That is the whole release-readiness predicate — policy release-blocker-gate —
and it is enforced rather than remembered: the release.cut standard in the Governance package
requires Gate.NoOpenIssues(repo, "sev:B") and Gate.NoOpenIssues(repo, "sev:H") for each of
the seven gated repositories — 14 gates — so a release proposal cannot reach Ready while one is
open. The pattern it uses is the long-running check (get Governance/Skill/long-running-check); see
Release Process for what a release then is.
Two rules that keep the gate honest
1. A blocking severity is never assigned by guess — and never REMOVED to clear the gate. If you
cannot prove from the evidence that a defect blocks the release, assign the severity you can justify
and say why. 🚨 Raising the bar to sev:H creates a pressure the sev:B-only gate never had: the
cheapest way to a green gate is now to downgrade an H to M. That is the one move this gate cannot
survive, so release.cut's acceptance criterion names it — not relabelled or DOWNGRADED to sev:M to
clear the gate. A gate that fills with
defensive B's stops being a gate — people start shipping past it, and then the one real B ships
too. Under-calling is recoverable because someone raises it; over-calling destroys the signal.
2. Re-run the query, and check its coverage. A zero means either "nothing blocking" or "truncated, unanchored, rate-limited, or answered from a subset". Those are indistinguishable from the count alone, and this is the one place where a false zero ships a known blocker. Read the count against the coverage; treat a refusal as an error, never as a pass.
What this gate does NOT decide
It says nothing about whether the artifacts exist. "Is every package available for this release?" is a different predicate with its own page — Release Availability Gates — and the two are complementary: one asks whether the product is built, this one asks whether it is fit to ship. A release needs both, and neither substitutes for the other.
It also does not restate what release.yml already refuses at tag push (an unsealed set, an
unpromoted commit, a mismatched PlatformVersion, missing release notes). A gate that duplicates
the lane is a second source of truth that will drift.
Where the tracking lives
GitHub is the ledger — the one place an issue is WRITTEN. The mesh reads it, and adds what GitHub cannot hold.
| holds | written by | |
|---|---|---|
| GitHub issues | what must be done — the labelled queue, and the gate query | people and triage |
GitHubIssue at {space}/_Issue/{number} |
a one-way mirror of the ledger, refreshed by sync and by webhook | system-security |
Feedback/Feedback |
intake — a finding as it arrives, before it is a work item | agents, users |
Hosting/TriageItem |
what is being done — the thread a defect is worked in, and what came of it (threadPath, status, issueUrl) |
the triage agent |
🚨 The mirror already exists and is one-way by design. MeshWeaver.GitSync's IssueService
materialises every issue as a GitHubIssue node under the space's _Issue satellite, refreshed by
an explicit sync and by a live webhook. The node is never edited directly: a mutation runs
against GitHub and the mirror re-syncs. So there is exactly one writer, and none of the
bi-directional problems a two-way mirror would bring.
That one-way direction is the load-bearing part. Making the mirror writable would put two writers
over one set of rows for no gain — PRs close GitHub issues natively, and a NoOpenIssues gate reads
GitHub directly, so nothing downstream needs the copy to be authoritative.
⚠️
Hosting/Issueis not part of this. It is fleet health — "no replicas ready", "not observed" — machine-written, self-resolving, one node per deployment × condition. It carries its ownSeverity(Warning/Critical, set by the detector), which is unrelated to thesev:scale here: that one grades a live deployment symptom, this one grades a defect in the product.
Scope: the OPEN set, and only the open set
Closed issues are out of scope. They are not classified, not counted, and no query on this page
looks at them — policy issue-taxonomy-scope. Every query here carries
state:open, including the gate.
That is affordable because the open set is hundreds, not thousands — 127 across five repositories on the day this was written, which is why the whole of it could be classified in a single pass and kept classified since. A taxonomy is worth maintaining at a scale where every open row can carry it; a backlog that outgrew that would be a backlog problem, not a labelling one.
The corollary for an agent: do not go back over history. Classify what is open, keep it classified as triage files new work, and let closure — not labelling — dispose of what is dead.
The first classification pass — 2026-09-20
All 127 open issues across the five repositories were classified in one pass.
| bugs | sev:B |
sev:H |
sev:M |
sev:L |
|
|---|---|---|---|---|---|
| MeshWeaver | 63 | 0 | 18 | 32 | 13 |
| MeshWeaver.Plugins | 21 | 0 | 11 | 8 | 2 |
| Crm · Reinsurance · Education | 2 | 0 | 0 | 2 | 0 |
Read the zero carefully. The classifiers were instructed never to guess B, so it means
nothing was proven blocking — not nothing blocks. On the day the taxonomy was introduced the
gate was already open, which is a finding rather than a success: a gate that has never refused
anything has not yet been tested.
Several sev:H calls sit close to the line and deserve a human ruling — a half-finished migration
that gates portal boot, an install whose verify can never succeed, a publicRead that would
publish every future user submission. The rubric says each of those could be B; the evidence in
the issue did not prove it. That is exactly the judgement the first rule above reserves for a
person.
That ruling was made on 2026-09-20: the bar is no
sev:Hleft. It settles the question this paragraph opened without needing a per-issue re-litigation — the three calls above block a release either way now. It also makes the gate non-vacuous for the first time: on the day the bar moved,sev:Bwas 0 across all nine repositories whilesev:Hstood at 30 (MeshWeaver 18, MeshWeaver.Plugins 12, every other repo 0). A gate that refuses something is a gate.🚨 These are a SECOND, later snapshot, not a correction of the table above. The first-pass table records the classification as it stood when the pass finished (Plugins
sev:H= 11); this count was read from the live labels at ~12:35Z the same day, when the bar moved, by which time Plugins carried 12. Both are true of their instant. The number that gates a release is never either of them — it is whateverrelease.cut'sNoOpenIssuesgates read when the cut is proposed.