# ktg-plugin-marketplace (catalog) Catalog repository for the ktg-plugin-marketplace. After the polyrepo migration this repo hosts only the marketplace manifest and the catalog-level docs; every plugin and the shared design-system live in their own Forgejo repositories under `https://git.fromaitochitta.com/open/`. ## What lives here - `.claude-plugin/marketplace.json` — the marketplace manifest (plugin entries point at external repos) - `README.md` — the landing/catalog page - `CONVENTIONS.md` — marketplace-wide conventions inherited by every plugin repo - `GOVERNANCE.md` — governance + fork-and-own model - `.mailmap`, `.gitleaks.toml`, `.gitleaksignore` — shared git-hygiene baselines ## Catalog maintenance - Marketplace conventions: see CONVENTIONS.md. - Adding/updating a plugin entry: edit `.claude-plugin/marketplace.json` (external `source: "url"` with a pinned `ref`) and re-state the plugin in README.md with its verified version. - Plugin source, issues, and releases live in each plugin's own repository — not here. - **Releasing a plugin (canonical path — `scripts/release-plugin.mjs`):** since the polyrepo split, a release is a TWO-repo act — tag the plugin repo AND bump the catalog `ref`. Forgetting the second step strands users on the old version (the exact drift this helper exists to prevent). Run `node scripts/release-plugin.mjs [--version X.Y.Z]` — dry-run by default; it REFUSES unless `plugin.json` == README badge == the target version AND the `vX.Y.Z` tag exists, then prints the planned bump. Apply with `--write [--commit] [--push]`; `--create-tag --write` mints+pushes a missing plugin tag first (**`--create-tag` is a WRITE and obeys `--write`** — on a dry-run it only reports what it would mint). On `--write` it bumps the catalog `ref` AND the catalog README's per-plugin `` `vX.Y.Z` `` label together (and `git add`s both on `--commit`). Because it only moves both to a verified, tagged, consistent version, `check-versions.mjs` is green by construction. Never hand-edit a `ref` or a README label for a release — use this. Pure planner + label reconciler + pre-flight/write step covered by `scripts/release-plugin.test.mjs`. - **Pre-flight gate (`--write` runs `check-versions` BEFORE it writes):** the helper calls `runGate()` first and aborts with exit 1 — **nothing written** — if ANY plugin is ERROR, not just the one being released (`check-versions`' exit code is catalog-wide). Previously the gate ran *after* both writes, so a red catalog left a half-applied release in the working tree for a parallel session to carry to the public remote. The pre-flight reads the **ERROR set only**, never `failed`/`--strict`: pre-bump, the plugin being released is *supposed* to be WARN (catalog `ref` behind `plugin.json`), so gating on WARN would brick every release. The post-write gate at the end stays — pre-flight validates the old state, that one validates the new state. **`--create-tag` is deliberately NOT behind this gate** (decided 2026-08-10): it mints and pushes the plugin tag *before* the pre-flight runs, but every precondition it checks is local to that one plugin (`plugin.json` == target, badge agrees, tag absent), so the tag is correct by construction. A red *other* plugin can only make the tag EARLY, never WRONG, and the tag-absent check makes the retry idempotent — gating it would let plugin Y block the *tagging* of plugin X, the same over-coupling that reading ERROR-only avoids. What WAS closed is the worse half: `--create-tag` used to push on the documented dry-run path, with no `--write` at all. It now requires `--write` (`shouldCreateTag`, tested). - **Version-consistency gate:** run `node scripts/check-versions.mjs` before committing any `ref` change. For each plugin it checks (against the sibling repo) that the catalog `ref` resolves to a real git tag (ERROR if dangling — breaks install), that `plugin.json` version == README version-badge (ERROR), that the catalog README's per-plugin `` `vX.Y.Z` `` label == the catalog `ref` (ERROR — the human-facing doc must not misstate the installed version), and that the catalog `ref` matches `plugin.json` version (WARN — catalog lags or an unreleased bump). Exit 1 on any ERROR; `--strict` also fails on WARN. Pure-function core covered by `scripts/check-versions.test.mjs` (`node --test scripts/check-versions.test.mjs`). - **Stat-badge mirroring (part of the same gate):** each plugin block in the catalog README ends in a stat line (`7 agents · 16 scanners · 21 commands · 1398 tests · [Full documentation →]`). The gate compares every number on that line against the plugin's own shields badge for the same axis, and ERRORs when they disagree — the catalog must not overstate a plugin. **The rule is per-AXIS, not per-plugin:** an axis the plugin does not badge is skipped silently, so there is no exception list to maintain. **Re-measured 2026-08-04 at the pinned refs, all 12 plugins: 42 catalog axis-claims — 27 badge-covered · 15 badge-less across 7 of the 12 plugins** and therefore ungated. That supersedes the 2026-08-02 reading (`24 · 15 across 7 of 11`); only two entries moved in between and both reconcile exactly — repo-mailbox now badges the two axes it did not (`+2` gated, `-2` ungated) and repo-standard entered the catalog (`+1` gated, `+2` ungated). The 15 ungated axes were hand-audited at their pinned tags — 13 on 2026-08-02 (**14 of that pass's 15 were exact**; the defects were repo-mailbox `6 CLI scripts` (true 8) / `251 selftest checks`, and voyage `500+ tests` against a measured 832), and repo-standard's two on 2026-08-04. Every ungated value currently stands measured at the ref it is pinned to — verified on **both** sides 2026-08-04, because a ref that has not moved only proves the *badge source* is unchanged and catalog prose is itself ungated: no ungated plugin's ref has moved since its audit, **and** diffing `extractCatalogStats` against the 08-02 README shows the only ungated numbers the catalog has restated since are repo-standard's two (absent then). **⚠️ The `251 selftest checks` correction is not fully settled**, though the weight is on 370: this file *and* the catalog's own 08-02 stat line both record the axis as corrected to 370, `check-versions.mjs` said 374 (most likely a transcription slip), and repo-mailbox's README at v0.19.0 implies 390 (`183 + 134 + 73`) — its own prose being ungated too. Moot for the gate (they badge the axis now, 398 at v0.20.2, green) and **not** re-measured. It stands as the proof of the cost: an ungated number rots, and so does the record of having fixed it. `N+` in the catalog is read as a lower bound, not an equality, so `500+` was never gate-visible — ungated axes rot in silence and need a re-run of this pass **whenever a ref moves**. **Never hand-edit a stat line to silence the gate** — the plugin's badge is the source for every stat number; fix the catalog to match it. - **Counting rules for a badge-less axis (calibrated against the badged plugins, 2026-08-02).** When the catalog must count an axis itself, count it the way the badges do, or the numbers stop being comparable across plugin blocks: **hooks = hook COMMAND entries** in `hooks/hooks.json` (not events, not matchers — the three diverge, and `commands` is what matched the badge on all 6 badged plugins), and **tests = `ℹ tests` from `node --test`**, not `ℹ pass` (config-audit's badge 1398 is its `tests` count; `pass` was 1375). Measure in an extraction of the tag (`git archive | tar -x -C `), never the sibling working tree. - **The stat mirror reads the plugin README AT THE PINNED `ref`, never the sibling working tree.** The catalog documents what *installs*, and that is the tag. A plugin that commits past its tag without bumping its version — measured 2026-08-02 on both llm-security (scanners 23→22, tests 2013→2034) and config-audit (tests 1398→1441) — would otherwise make the gate demand that the catalog restate unreleased numbers, which is exactly backwards. **When the gate flags a stat, check `git show :README.md` in the plugin repo before believing the working tree.**