applyRelease()'s `try` spanned both the catalog README read AND its write, so a
real EACCES/ENOSPC on the write was swallowed and reported as readme:'missing'
("no catalog README to update") with verdict WROTE and exit 0 — a bumped ref
with a stale label, announced as success. Pre-existing form, not a regression.
The `try` now covers the READ only: a catalog without a README stays a tolerated
state, a README that cannot be written throws.
Known narrow guarantee: the throw propagates out of main() AFTER marketplace.json
is written, so the working tree is left half-applied (bumped ref, stale label).
That is the same hazard ac7ad42 closed for gate ordering — but strictly better
than today's silent exit 0, and widening the fix is not in scope here.
Test is path-selective on purpose: a fake that throws for every path dies on the
marketplace write above the try (in both the old and the new code) and would go
green against the unfixed file. Verified red before the fix.
Tests 25 -> 26 (suite total 131 -> 132).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A839v1MGa3Bm8icX8dTHYi
`--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
release-plugin.mjs --write wrote marketplace.json and the catalog README label
first, and only THEN ran check-versions via execFileSync (which throws on exit
1). A red catalog therefore left a half-applied release in the working tree —
exactly the state a parallel session has already been observed carrying to the
public remote.
Adds applyRelease() with an injected io seam so the ORDER is testable: runGate()
runs first, and any ERROR aborts with nothing written. The pre-flight reads the
ERROR set only, never failed/--strict — pre-bump the released plugin is SUPPOSED
to be WARN (catalog ref behind plugin.json), so a WARN gate would brick every
release. Verified against the real classifier, not synthetic data.
The post-write gate stays: pre-flight validates the old state, that one
validates the new state.
Known remaining hole, documented not built: --create-tag mints and pushes the
plugin tag before the pre-flight runs.
Tests 14 -> 19 (120 -> 125 across the six suites); check-versions 12 OK.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018Ga5tZ3AgUxAcdLtWB8Kig
The catalog README's per-plugin `vX.Y.Z` labels were an UNGUARDED surface:
check-versions validated each plugin's OWN README badge, never the catalog
README's labels, so they drifted (config-audit shown v5.5.0 while pinned to
v5.7.0; voyage v5.1.1 while pinned to v5.6.0) — the doc misstated what
`claude plugin install` actually resolves.
Close the class, same pattern as the ref surface:
- check-versions.mjs: new ERROR rule "catalog README label == catalog ref"
via pure extractCatalogLabel() (matches the /open/<name>) heading, takes the
first `vX.Y.Z`, ignores a trailing lang/flag badge). No legitimate transient
state lets label != ref, so ERROR (not WARN). +5 tests.
- release-plugin.mjs: on --write, bump the README label atomically with the ref
via pure reconcileReadmeLabel() and git add README.md on --commit, so future
releases keep label and ref in lock-step. +4 tests.
- README.md: reconcile the 2 stale labels (config-audit -> v5.7.0,
voyage -> v5.6.0). Gate now 9 OK / 1 WARN / 0 ERROR.
- CLAUDE.md: doctrine updated to document the new gate rule + atomic label bump.
ms-ai-architect stays WARN by decision (1.16.0 is unreleased WIP, never tagged).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Cm2RxKbomdLqjiWGcwCCPi
The polyrepo split made a plugin release a TWO-repo act: tag the plugin repo AND
bump the catalog `ref`. The second step is manual and easily forgotten — that
drift just stranded linkedin-studio on v0.4.0 while its plugin.json moved to
0.5.0. This adds the canonical release path that makes the catalog side
impossible to do wrong.
scripts/release-plugin.mjs:
- Pure planner planRelease() — given the catalog + observed plugin state +
target version, computes verdict (READY/NOOP/BLOCKED), the bumped marketplace
object, and the commit subject. REFUSES (BLOCKED) unless plugin.json == README
badge == target AND the vX.Y.Z tag exists, so a READY plan is check-versions-
green by construction. Reuses normalizeVersion/classifyPlugin from
check-versions.mjs (no duplicated rules).
- I/O shell: dry-run by default; --write bumps the ref + re-runs the gate;
--commit/--push apply; --create-tag mints+pushes a missing plugin tag first.
- 10/10 unit tests (scripts/release-plugin.test.mjs); check-versions 9/9 still green.
- Dogfooded: linkedin-studio → NOOP (already pinned v0.5.0); ms-ai-architect →
BLOCKED (v1.16.0 in plugin.json was never tagged — refuses to publish it).
Also: fix the stale LinkedIn Studio README label v0.4.0 -> v0.5.0 (loose end of
the v0.5.0 release), and document the canonical release path in CLAUDE.md.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>