Order 20260912T222008Z-8793921535-from-.claude (Q3d), follow-up to the PM
re-measurement of Q3c (dd278ca).
R1 — the run-scoped push-token consumption was only test-vouched at the
runRelease() level; D3's own test called consume() a second time in the TEST
BODY ("mirrors main()'s own finally") and asserted on that call, never on
anything main() itself did. A PM agent deleted the `finally` line in main()
in a copy and every existing test stayed green (39/39) while the real CLI,
run end-to-end, pushed the tag, hit NOOP, and left the token behind.
Added a test that runs the actual CLI entry point as a real subprocess
(main() calls process.exit(), so it cannot run in-process without killing
the test runner) against an isolated plugin repo, a local bare "remote",
and its own HOME — the same scenario the PM agent used (tag pushed, then a
NOOP branch that is not the run's final line). Mutation proof: removed the
`finally` line -> new test went RED (40 pass, 1 fail) while D3 stayed GREEN,
confirming D3 does not cover this path -> restored -> GREEN.
Sub-fix required to make the new test possible on this machine: macOS's
os.tmpdir() resolves through /var/folders, a symlink to /private/var/folders.
release-plugin.mjs's self-invocation guard compares the literal argv[1] path
against import.meta.url (which Node resolves through symlinks), so a script
run from the unresolved path never satisfies the guard and main() silently
never executes (exit 0, zero output). makeTempRoot() now returns the
realpath of the created temp dir.
S (side finding) — a tag push that fails after the local `git tag -a`
succeeded left an orphan local tag behind; a retry then failed on git's own
"tag already exists" (exit 128) instead of going through the idempotent
tag-absent path --create-tag already relies on. Chose: delete the local tag
when its push fails (option a) rather than detect-and-explain the orphan
state (option b) — it reuses the existing idempotency property instead of
adding a second one. Red-first: new test failed (orphan tag survived) ->
wrapped the push in try/catch, `git tag -d` on failure, rethrow -> GREEN.
R2 — CLAUDE.md said "a failed push leaves the token intact for the retry",
which is imprecise: with --create-tag --write --commit --push, if the tag
push succeeds and the catalog push then fails, the token IS consumed
(main()'s finally fires because pushGate.pushed was already set true by the
earlier successful push) even though the run overall "failed". Corrected to
state the actual rule: the token survives only when the run makes zero
successful pushes. The usage-block comment at the top of release-plugin.mjs
does not carry the same imprecise claim, so it needed no change.
Verification:
- node --test scripts/release-plugin.test.mjs: 39 -> 41/41 (R1, S added)
- node --test scripts/*.test.mjs: 156 -> 158/158
- node scripts/check-versions.mjs: 0 ERROR (1 known WARN: claude-design;
repo-mailbox now OK — externally re-tagged since Q3c, untouched here)
- git tag -l: unchanged (12 tags, no new ones — no tag/push/bump this session)
- mutation proof for R1: finally line removed -> RED (D3 stayed green) -> restored -> GREEN
- mutation proof for S: recorded in scripts/release-plugin.test.mjs history above (red-first)
Not done (out of scope, deliberately): no version bump, no tag, no push;
no files touched outside scripts/release-plugin.mjs, scripts/release-plugin.test.mjs, CLAUDE.md.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
21 KiB
21 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.
- Every tag this repo cuts is ANNOTATED —
git tag -a, never a baregit tag. A lightweight tag is a branch-like ref: it can be moved to another commit with nothing recorded that it ever pointed elsewhere. The catalog pins every plugin toref: vX.Y.Z, so a movable tag is a movable pin — a supply chain property, not a tidiness one (repo-standardTAG-ANNOTATED, ERROR). Two tagging sites, and only one of them was ever enforced:- Plugin tags are cut by
scripts/release-plugin.mjs(--create-tag --write), which has usedgit tag -asince its first commit (9b1838f) and has never had the defect. Verified 2026-08-17 against the published surface, not the source:git ls-remote --tagson repo-mailbox returns bothrefs/tags/v0.25.0(tag objecta9b5eb8) and the peeledrefs/tags/v0.25.0^{}(commit95ac710) — a lightweight tag has no peeled ref. Do not "fix"release-plugin.mjs; it is not the drift site. - The catalog's own tags are cut BY HAND, and that is the site with no enforcement. All four
newest (
v7.7.2,v7.7.1,v7.7.0,v5.0.3) are lightweight, from the monorepo era. Control proving the measurement discriminates:pre-polyrepo-archivereportstag, and a local probe pair (git tag -a→tag,git update-ref refs/tags/x→commit) reproduces both outcomes. The published lightweight tags STAY (decided 2026-08-17, forward-only). The only remedy for an already-published tag isgit tag -a -f <name> <name>^{}, which force-moves a ref others may have fetched — precisely the traceless move the finding warns about. Applying it as the fix would demonstrate the defect. Consequence to state plainly, not to hide:TAG-ANNOTATEDstays ERROR until a NEW annotated tag becomes the newest, becausecheckTagIntegrity'stags_lightweight_acceptedregister key reaches tag HISTORY only and cannot excuse the newest tag. And "newest" iscompareTagsorder (semver triple), so a fresh-startv1.0.0would NOT clear it — a clearing tag must sort abovev7.7.2. Non-semver tags (config-audit/v5.0.0,pre-polyrepo-archive) parse to[0,0,0,…]and never rank. The ERROR is one the org ALREADY ACCEPTED, and the acceptance cannot land — measured 2026-08-17. repo-standard's register (register/repos.json:231) listsv7.7.2undertags_lightweight_accepted["ktg-plugin-marketplace"], accepted 2026-08-14 with a rationale recorded at:211-223: it is a monorepo-era llm-security tag (2026-05-19, pre-split), not a catalog release; the README install block pins no ref and all 12marketplace.jsonrefs are plugin-repo tags, so zero install paths consume it. ButcheckTagIntegrityjudges the newest tag at:711before it reads the register at:719, andacceptedis only ever applied totags.slice(0, -1). So the entry forv7.7.2is inert: the decision is written down and the gate cannot honour it. Run against this repo (importing repo-standard's own exportedcheckTagIntegrity, its real register, andgit for-each-ref refs/tags/v*), the catalog scores exactly two findings:[ERROR] TAG-ANNOTATEDonv7.7.2, and[OK] TAG-ANNOTATED-ACCEPTEDcovering the other 7 lightweight tags. There is noTAG-ANNOTATED-HISTORYWARN — history is fully claimed. Consequence for whoever reads the next org-wide run: this repo's single ERROR is not an unmade decision, it is a made decision the checker structurally cannot see. That is the same "we decided this" / "nobody looked" collapse the register exists to prevent, one level up. The fix belongs in repo-standard, not here — reported, not patched. Note:~/.gitconfigsetstag.gpgsign true, which makes a baregit tag <name>fail ("no tag message?") rather than silently cut a lightweight one. Measured 2026-08-17, the probe's-atag came out unsigned all the same (0 PGP blocks) — so this config is an accident that happens to block one path, not a signing guarantee. Do not treat it as the enforcement.
- Plugin tags are cut by
- 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.--create-tag --writeand--pusheach require the operator's push-approval token FIRST (Q3, decided 2026-09-12):~/.claude/hooks/pre-push-gate.shmatchesgit pushin command text and cannot see a push this script issues viaexecFileSyncinside node — the script mints+pushes a plugin tag and pushes the catalog itself, both invisible to that gate. So before either push, run:mkdir -p ~/.claude/runtime/push-approvals && touch "~/.claude/runtime/push-approvals/$(pwd | sed 's|/|_|g')"(pwdmust be this catalog directory — tag-push and catalog-push share ONE token, one publish from the operator's perspective). The script consumes the token itself right after a push succeeds, the same waypost-push-consume.shdoes for a direct push; the token stays intact for a retry only when the run makes ZERO successful pushes — once any push in the run succeeds (e.g. the tag push in--create-tag --write --commit --push), the token is spent even if a later push in that same run then fails. Covered bypushAuthorisation/requirePushAuthorisation/pushWithToken/consumeTokeninscripts/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-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-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), that the catalogrefmatchesplugin.jsonversion (WARN — catalog lags or an unreleased bump), and thatplugin.json'shomepage, if present, actually resolves (ERROR if dead — a livecurlcheck, synchronous by design, see the comment atcheckHomepagefor why it must never become async). 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).homepageis OPTIONAL, not required (decided 2026-08-26): measured that 11 of 12 plugins omit the field. Requiring it would flip those 11 to ERROR and, viarelease-plugin.mjs's ERROR-set pre-flight (catalog-wide), block every release in the marketplace until each of those 11 sibling repos re-tags — a marketplace-wide freeze delivered by a script fix. The gate only fires when the field is present AND resolves to a definitive 4xx/5xx; a network failure (timeout/DNS) reads as "not checked", never as a confirmed dead link.- SKIP is UNVERIFIED, not clean, and now blocks the gate by default. Measured 2026-08-18: a
fresh clone with zero sibling repos printed
12 plugins — 0 OK, 0 WARN, 0 ERROR, 12 SKIPat exit 0 — a run that verified nothing was indistinguishable from a run that verified everything and found it clean (Verifiseringsloven ansikt 4, in the org's own release gate). Any SKIP now fails the run unless--allow-skipis passed explicitly, and the summary line always reports the denominator:— verified N/M.runGate'shasError/hasWarn/failedfields are unchanged in meaning; the new SKIP-based failure is a separateunverifiedfield folded intofailed—release-plugin.mjs's pre-flight (preflightErrors) reads the ERROR set directly offresults, neverfailed, so it is untouched by this.
- 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-13 at the pinned refs, all 12 plugins: 42 catalog axis-claims — 24 badge-covered · 18 badge-less across 8 of the 12 plugins and therefore ungated, with 0 disagreements on the gated set. The measurement importscheck-versions.mjs's OWN exportedextractStatBadges/extractCatalogStatsand feeds themgit show <ref>:README.md, so coverage is read by the same code that gates it rather than by a second parser that could drift. The prior27 · 15 across 7(2026-08-04) was correct and is not overturned — re-running the measurement against catalog commitc7fbbd3reproduces27 · 15 across 7exactly. Coverage FELL because three axes lost their badge when a ref moved, each verified at both tags:okrv1.8.2 → v1.10.0 droppedagents-7,hooks-3andreferences-17(the catalog restates the first two → −2 gated), andgraceful-handoffv3.1.0 → v3.2.1 droppedtests-30andhooks-0(the catalog restates onlytest→ −1 gated). 27 − 3 = 24, 15 + 3 = 18, +1 ungated plugin (okr). This is the CLAUDE.md-documented graceful-handoff badge-drop reaching a release: it was visible onmainon 2026-08-04 and is now what installs. All 18 ungated values were measured at their refs in the same pass (2026-08-13) — 16 exact, 1 correct-but-split, 1 defect. Exact: voyage24 agents/7 hooks/832 tests, linkedin-studio6 skills, graceful-handoff1 pipeline/48 tests, ai-psychosis1 skill/1 command, ms-ai-architect29 commands/5 skills/2 hooks, okr7 agents/3 hooks, claude-design5 tests. Split: voyage6 commands (+1 helper)measures 7 command files, and the split is the plugin's own — its README atv5.9.1reads "6 slash commands (…) + trekendsession helper", and that file'sdescription:calls itself a helper. Defect: repo-standard170 testsmeasures 243 atv0.11.1(node --test scripts/*.test.mjs, the version itspackage.jsondeclares). The catalog is faithfully mirroring repo-standard's own README, which says "170 tests over the pure classifiers" at that tag — so the plugin's prose is the stale source and the fix belongs there first. Understating, not overstating, which is why nothing screamed. RESOLVED 2026-08-14/15 (commit3f5afee): repo-standard's checks axis is now consistent at 20. The three-way disagreement atv0.11.1— catalog bullet said "Twelve", catalog stat line said 14, plugin's own README said "twelve checks" — is gone, and it was resolved on repo-standard's side, not by the catalog picking a number:v0.11.2's CHANGELOG records the plugin removing its own stale prose counts ("170 tests" and "twelve checks") rather than correcting them, making the README's own "Check | What fails it" table the sole source of truth. That table is unchanged betweenv0.11.1andv0.11.2(verified:diffof the table is empty) and has 20 rows, counted independently this session (git show v0.11.2:README.mdin the sibling repo, First screen through Description). The catalog's repo-standard block (README.md:206bullet,README.md:212stat line) now reads "20 checks" in both places, matching. The earlier "19 exportedcheck*functions" count was a different unit (exported functions, not documented table rows) and is superseded by the table now being canonical — no further ask to repo-standard is needed. ⚠️ 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 was not re-measured for years. Re-measured 2026-09-05 against the now-pinnedv0.33.1: badge readsselftest_checks-868(git show v0.33.1:README.mdin the sibling repo), the catalog's own stat line already agrees (corrected 09-04), gate green. Repo-mailbox's own listed per-script counts still only sum to 792 (220+360+73+99+40) against the 868 badge — the same badge-less-prose pattern recurring. 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 ENTRIES in
hooks/hooks.json(not events, not matchers — the three diverge), 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.- Re-calibrated 2026-08-13 — the file-counting rules now have a measured referent, 11/11.
agents=agents/**/*.md(4/4: llm-security 6, config-audit 7, ms-ai-architect 12, linkedin-studio 20),commands=commands/**/*.md(3/3: config-audit 21, okr 16, linkedin-studio 30),skills=skills/*/SKILL.md(4/4: repo-mailbox 3, claude-design 1, graceful-handoff 1, repo-standard 1).tests = ℹ testsre-confirmed against config-audit (ℹ tests 1398== badge 1398,pass1374 — counttests, and note the suite need not be green for the census to be valid).scannersis NOT calibrated: neither files-under-scanners/(llm-security 55, config-audit 61) nor top-level.mjs(27 / 32) reproduces the badges (23 / 16), so the axis counts something the tree does not name. Harmless today — both scanner claims are gated and there is no ungated one to count — but do not invent a rule if that changes. - ⚠️
git ls-treeQUOTES non-ASCII paths, and a naive count silently drops them. okr'scommands/innføring.mdandcommands/møter.mdcome back as"commands/innf\303\270ring.md", so a^commands/.+\.md$match counted 14 against a badge of 16 and read as a defect in okr. It was a defect in the measurement. Always pass-c core.quotePath=false. The same pass also produced a fully bogus "13/13 MATCH" from a zsh loop whereset -- $specdid not word-split, leaving every field empty so"" == ""passed — a verification that cannot fail has verified nothing. Count in Node, not in a shell loop. type: promptentries COUNT (decided 2026-08-13). The rule read "hook COMMAND entries" until now. That wording was calibrated in 2026-08 against plugins that predate prompt-hooks, so it was never a ruling on them — it had no case to rule on. A prompt entry fires on the same event and does the same job from the reader's side, so excluding it would understate the plugin. Re-measured 2026-08-13 at all 12 pinned refs before the rewrite, because restating an old measurement in new words is itself a claim: every plugin that both badgeshooksand ships ahooks.jsonhas entries == command-entries (llm-security 9/9, config-audit 4/4, linkedin-studio 9/9, ai-psychosis 4/4, repo-mailbox 1/1), so the two phrasings agree on the whole badged set and the rewrite contradicts nothing.okr@v1.10.0is the only plugin with a non-command entry — 3 entries, 2command+ 1prompt(PreCompact) — and it does not badge the axis, which is precisely why the badged set never tested the rule. The catalog's3 hooksstands, measured.- The
agentsaxis IS badge-covered — 4 of the 6 plugins that claim it (measured 2026-08-13).llm-securityagents-6,config-auditagents-7,ms-ai-architectagents-12,linkedin-studioagents-20; all four match the catalog exactly and are gated. Ungated:voyage(24 agents) andokr(7 agents) — badge-less. Both measured 2026-08-13 at their refs: 24 and 7, exact. Do not record this axis as uncalibrated: it has a referent. - A badge whose value is not an integer does not gate the axis.
extractStatBadgeskeeps only integer-valued badges, so graceful-handoff'sPipeline/STATE--helper-deterministicbadge leaves1 pipelineungated even though a badge for the axis visibly exists. When asking "is this axis gated?", read the badge's VALUE, not its label.
- Re-calibrated 2026-08-13 — the file-counting rules now have a measured referent, 11/11.
- 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.