`--create-tag` minted AND PUSHED a plugin tag to a public remote without
`--write`, on the entry point CLAUDE.md documents as "dry-run by default".
The tag was public before the plan was even printed.
Extracts `shouldCreateTag(args, observed, target)` as a pure exported
predicate ('create' | 'dry-run' | 'skip') so the flag's write-ness is
testable, and gates minting on `--write`. Without it the CLI now reports
what it would mint, printed after the missing-tag blocker that points at
the flag.
Deliberately NOT placed behind the catalog-wide pre-flight: every
precondition it checks is local to the plugin being released
(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 the ERROR set only (never `failed`) exists to avoid.
Tests 19 -> 25; 131/131 across the six suites. check-versions 12 OK.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019i3rnU2VNRdGcrUFMnRb6R
8.1 KiB
8.1 KiB
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 pageCONVENTIONS.md— marketplace-wide conventions inherited by every plugin repoGOVERNANCE.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(externalsource: "url"with a pinnedref) 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 catalogref. Forgetting the second step strands users on the old version (the exact drift this helper exists to prevent). Runnode scripts/release-plugin.mjs <plugin> [--version X.Y.Z]— dry-run by default; it REFUSES unlessplugin.json== README badge == the target version AND thevX.Y.Ztag exists, then prints the planned bump. Apply with--write [--commit] [--push];--create-tag --writemints+pushes a missing plugin tag first (--create-tagis a WRITE and obeys--write— on a dry-run it only reports what it would mint). On--writeit bumps the catalogrefAND the catalog README's per-plugin`vX.Y.Z`label together (andgit adds both on--commit). Because it only moves both to a verified, tagged, consistent version,check-versions.mjsis green by construction. Never hand-edit arefor a README label for a release — use this. Pure planner + label reconciler + pre-flight/write step covered byscripts/release-plugin.test.mjs. - Pre-flight gate (
--writerunscheck-versionsBEFORE it writes): the helper callsrunGate()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, neverfailed/--strict: pre-bump, the plugin being released is supposed to be WARN (catalogrefbehindplugin.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-tagis deliberately NOT behind this gate (decided 2026-08-11): 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-tagused to push on the documented dry-run path, with no--writeat all. It now requires--write(shouldCreateTag, tested). - Version-consistency gate: run
node scripts/check-versions.mjsbefore committing anyrefchange. For each plugin it checks (against the sibling repo) that the catalogrefresolves to a real git tag (ERROR if dangling — breaks install), thatplugin.jsonversion == README version-badge (ERROR), that the catalog README's per-plugin`vX.Y.Z`label == the catalogref(ERROR — the human-facing doc must not misstate the installed version), and that the catalogrefmatchesplugin.jsonversion (WARN — catalog lags or an unreleased bump). Exit 1 on any ERROR;--strictalso fails on WARN. Pure-function core covered byscripts/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 (+2gated,-2ungated) and repo-standard entered the catalog (+1gated,+2ungated). 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-mailbox6 CLI scripts(true 8) /251 selftest checks, and voyage500+ testsagainst 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 diffingextractCatalogStatsagainst the 08-02 README shows the only ungated numbers the catalog has restated since are repo-standard's two (absent then). ⚠️ The251 selftest checkscorrection 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.mjssaid 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, so500+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, andcommandsis what matched the badge on all 6 badged plugins), and tests =ℹ testsfromnode --test, notℹ pass(config-audit's badge 1398 is itstestscount;passwas 1375). Measure in an extraction of the tag (git archive <ref> | tar -x -C <tmp>), 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, checkgit show <ref>:README.mdin the plugin repo before believing the working tree.