Skip to content

/rhiza:quality

Run the Rhiza code-quality gate and score the current repo, then optionally file findings as issues.

/rhiza:quality [path or topic to scope the assessment to]

The optional argument scopes the assessment; it defaults to the whole repo.

Three modes, decided by what .rhiza/ holds

/quality checks for .rhiza/template.yml, .rhiza/template.lock and .rhiza/template-bundles.yml before doing anything, and adapts rather than refusing:

  • pointer + lock — full mode: the template's gates plus the design assessment.
  • template.yml only — degraded mode: managed but never synced, the state /rhiza:init deliberately leaves behind. /rhiza:update performs the first sync.
  • template-bundles.yml, no pointer — template mode: this repo is the template. See below.
  • none of them — degraded mode: the repo isn't rhiza-managed.

In degraded mode it skips every template-delivered gate, runs whatever targets your own Makefile provides, and scores the design work in full.

What it will not do is run the template's gates anyway. Every one is a gate the sync delivers, so in an unsynced repo they all fail with "No rule to make target" — and reporting that as FAIL would describe a broken repo when the truth is an unsynced one. Skipped gates are scored out-of-scope, never failures, and the report names which mode produced the number.

The second probe is the lock, deliberately: it is written by every sync at every template version, where a synced file is only ever a proxy for one. That proxy was .rhiza/rhiza.mk until template v1.4 retired the make layer and stopped shipping it — after which every correctly and fully synced v1.4 repo answered "never synced" and was quietly scored in the narrower mode.

Template mode — running /quality on the template repository

jebel-quant/rhiza has no pointer and no lock, because nothing is above it. Read naively that is "not rhiza-managed", and the command would offer /rhiza:init to the repository that ships the bundles /init points at. .rhiza/template-bundles.yml — the bundle-and-profile manifest a sync reads out of the template — is what says otherwise.

It runs like degraded mode, with the messaging corrected: no /rhiza:init or /rhiza:update suggestion, no "no Rhiza gate ran" boilerplate (the gates that ran are the ones every managed repo receives), and the infrastructure assessment is the main result rather than a fallback — those files are what consumers inherit. The "fix it upstream" escape hatch is gone too: in this repo, upstream is here.

What it does

  1. Runs the quality gates (cheapest first) — lint, types, docs, deps, security, template drift, tests, and the executable-documentation check. Because the available targets depend on the profile in template.yml (typecheck, security and docs-coverage come from the tests bundle), it probes each with make -n first; a target that isn't in the profile is scored out-of-scope, not FAIL.

    Four gates are the exception — fmt, typecheck, docs-coverage and deps resolve in any repo, because they are gated in most mature repos and reporting them unavailable understates coverage. Each tries its make target, then falls back to your own tool config: .pre-commit-config.yaml through its own runner, [tool.mypy], [tool.interrogate], [tool.deptry]. Every threshold still comes from your committed config — which is the thing that matters — and only the path is passed.

    No config means out-of-scope, never the template's flags. Copying mypy --strict onto a repo that never chose it measures a standard the repo didn't adopt and then files issues for it. Declining to gate something is a process finding, not a failure.

    A target merely named lint or format is deliberately not accepted as a stand-in either: the name doesn't tell you the scope. A repo's lint routinely runs mypy, interrogate and the contract checkers too, so scoring it as fmt would credit formatting with most of the toolchain. Documentation is checked for truth, not just presence. plugin/scripts/check_doc_examples.py runs alongside the gates in any repo: it finds the >>> examples in your docstrings and checks every fenced block in README.md — shell parses under bash -n, Python under compile(), and each >>> line of a pycon fence (a doctest transcript with the expected output inline) compiles too. --run additionally executes the examples — the README's pycon fences as one doctest, in document order — which imports your modules, so it is opt-in; shell fences are never executed at all.

    These are the same three checks a rhiza-managed repo already gets from make rhiza-test (.rhiza/tests/test_docstrings.py, test_readme.py, test_readme_validation.py), on the same +RHIZA_SKIP fence flag — so in full mode it adds an inventory rather than a second score, and in degraded mode it is the only thing asking whether your documented examples still work. A module it cannot import is reported unmeasured, never failed, and no examples at all is reported as a gap rather than a pass. 2. Gathers design evidence itself — complexity via radon cc/radon mi, plus an import-graph read for layering direction, cycles (including ones hidden behind function-local imports), god-modules and coupling hotspots. No make target measures these. 3. Scores 1–10 per subcategory, with an overall score and the single highest-leverage improvement called out. 4. Produces actionable findings — one per subcategory below 10, each with a self-contained title, the current→target score, the specific file(s)/config, a done when… criterion, and an evidence snippet, ordered by leverage. 5. Optionally files issues for them — via a multi-select menu, never free text, and nothing created without an explicit selection. Before the menu, it checks the tracker's open and closed issues, and labels each finding that matches one: already open, declined, recurring after a fix, or closed for a reason it can't see. Every finding is still offered, so you decide what to file with the history in view.

Your own rules: .rhiza/quality.md

A repo can state the standards it holds itself to, and /quality reads them before it runs anything. Write .rhiza/quality.md by hand — freeform markdown, no schema, nothing parses it — and put in it the four things a generic gate list cannot know:

# Quality charter

## Extra gates
- `make bench` — the performance suite. Must be green; a regression is a release blocker.

## Scope
- `legacy/` is being deleted this quarter. Don't score it and don't file against it.

## Accepted deviations
- The complexity ceiling doesn't apply to `tests/`. A branchy end-to-end fixture is
  branchy because the scenario is, and splitting it buys a grade at the cost of a test
  a human can follow.

## Emphasis
- We ship on Windows. Always score cross-platform robustness.
- Style nits are handled by the formatter in CI; please don't file issues for them.

Extra gates are run and reported beside the built-in ones. Scope clauses narrow what is scored and filed. An accepted deviation stops being re-filed every run — it is listed as accepted, with your reason quoted, so a reader sees what was excused and on what grounds. Emphasis decides what is always scored and what is ranked first.

What a charter cannot do — and why the limits are the point

It governs judgement, never the instruments. Thresholds stay in your committed config, because that is what CI enforces and therefore what the repo actually means. So a charter can raise a bar and cannot lower one: a markdown sentence that lowers a coverage floor is a threshold supplied at scoring time, which is the one thing /quality refuses everywhere else.

It also cannot hide a gate result (a FAIL stays a FAIL; your explanation lands next to it), cannot excuse a gap it gives no reason for, cannot grant the command permissions it doesn't have — it still never edits, commits, pushes, or files an issue without your explicit selection — and cannot suppress the lines saying which mode produced the number and what rested on a narrower base.

Every mark a charter moved is labelled where it moved, quoting the clause. A charter-adjusted score that read like an unadjusted one would be the same failure as a degraded-mode score that read like a full one.

A clause that asks for something out of bounds is named once, reported as unhonoured, and set aside — not silently followed, and not silently dropped.

A charter does not make a repo rhiza-managed: the mode is decided by .rhiza/template.yml and .rhiza/template.lock alone, so a .rhiza/ holding only a charter is an unmanaged repo that wrote one. It is honoured in all three modes. Full details in quality-charter.

Language support: the gate list is the Python profile

A Rust or Go scorecard rests on a narrower base

The numbered gate list above is the Python profile — the one this plugin has actually run against. On a Rust or Go repo most of those targets are unavailable, so /quality probes the Makefile with check_make_targets.py, scores the targets it discovers, and marks language-specific subcategories (gate 8's docstring half above all) out-of-scope rather than measuring them.

That is deliberate — a hand-written table of targets for templates the plugin has never run against would be prose asserting things it cannot back, and discovery degrades honestly where a guessed table would lie. But it means a Rust or Go score is not comparable to a Python one, so the command states in its own output which gates were discovered and which subcategories were skipped.

See Language support for what else differs — notably that neither Rust nor Go has hosted CI workflows yet.

Why gates run through make

Invoking the tools directly (uvx ruff check, uvx interrogate, …) would let /quality run anywhere, but the two disagree: measured against this plugin's own repo, bare interrogate reports FAILED at 99.5% where the configured hook passes, and bare bandit reports a high-severity finding where the configured hook passes. The arguments, thresholds and exclusions live in the make target and .pre-commit-config.yaml, so a direct call measures something else — and for a command whose output is a score and a findings list, that means inventing failures and filing issues for them.

The make target is also the entry point CI uses, which is what makes its verdict the one worth scoring.

Notes

  • It assesses; it does not fix. Failures are diagnosed and a fix proposed, never applied — a scoring run that quietly edits code makes its own score unreproducible. The only exception is whatever make fmt auto-formats as a side effect of running.
  • Respects the locally-owned vs. Rhiza-owned split, so template-managed files (Makefile, .pre-commit-config.yaml, ruff.toml, workflows) don't drive the marks — a gap there is flagged upstream, not scored against you.
  • /quality is the only command that scores. /rhiza:update used to invoke it and carry a scorecard in its PR; it no longer does, so that a template bump can't be polluted by make fmt rewriting your files. Run this yourself when you want a score.
  • Don't confuse make validate (repo drifted from the template) with plugin/scripts/validate.py, which /rhiza:status runs (is template.yml well-formed). A repo can pass one and fail the other.

Reference

Source plugin/skills/quality/SKILL.md
Invocation /rhiza:quality [path or topic to scope the assessment to] (optional; defaults to the whole repo)
Model-invocable yes
Allowed tools Bash(make*), Bash(git*), Bash(gh*), Bash(glab*), Bash(uv*), Bash(uvx*), Bash(python3*), Bash(grep*), Bash(find*), Bash(wc*), Bash(sed*), Bash(sort*), Bash(uniq*), Grep, Glob, Read, Edit, Write, AskUserQuestion