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.
How an issue reaches triage
An issue opened straight on GitHub reaches the triage agent by two routes, both on the control
instance, and both limited to the same triage scope (Hosting:Triage:Repositories: the active
fleet repositories by default; the control instance's record declares it). An issue in any other
repository is counted and logged, and gets no item and no thread.
- The organisation Issues webhook delivers
opened/reopenedto the control instance's inbox. - The periodic sweep (
TriageIssueSweep) lists every OPEN issue in scope that is missing a classification — including one that already existed, and one that lost itssev:or type label later, which the webhook deliberately ignores.
Each issue becomes ONE item, Hosting/Triage/issue/{repo}-{number}, whichever route found it
(foundBy). The mechanism, the settings and the idempotency rule are in MeshWeaver.Plugins'
Hosting/Triage (get Hosting/Triage). A finding an agent makes goes in by the third route,
Feedback/Feedback (AGENTS.md, "file it and move on"), never as a hand-opened issue.
⚠️
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.