Skip to content

ci: no gate detects a duplicate key in a plugin manifest — schema validation cannot, and every consumer is last-wins #1498

Description

@kyle-sexton

#1492 was a duplicate "version" member in plugins/skill-quality/.claude-plugin/plugin.json that shipped to main through a fully green 27-context CI suite. The manifest fix is #1494. This issue is the systemic half: no gate in CI can detect a duplicate key in a plugin manifest.

Verified, not assumed

Each of these was run against a probe manifest carrying two version members (0.11.0 then 0.12.0):

Check Result Why it cannot catch it
check-jsonschema + json.schemastore.org/claude-code-plugin-manifest.json (the hygiene job's Validate plugin manifests step) ok -- validation done, exit 0 JSON Schema has no vocabulary for duplicate object keys. Duplicates are a document-level concern, not a schema-level one, so no schemafile can express the constraint.
claude plugin validate <dir> (via scripts/validate-plugins.sh) exit 0, warned only about a missing author Parses last-wins; never inspects key uniqueness.
scripts/validate-plugin-contracts.mjs, scripts/generate-catalog.mjs pass JSON.parse is last-wins per ECMA-262.
changelog-parity-gate (scripts/check-changelog-parity.sh) pass Reads the version with jq -r '.version' — last-wins. This gate is also the one most harmed by the defect, since it compares a version it cannot trust.

So every consumer in the pipeline resolves the same last-wins value and reports the intended version whether or not the duplicate is present. The green suite was not a fluke — it is the expected behaviour of every tool involved.

Why this defect class keeps appearing

The conflict shape that produces it recurred three times in one night across #1085, #1096, and the CHANGELOG resolutions alongside them: two branches independently bump the same plugin to the same version, so the version line becomes common context outside the conflict region while only the neighbouring differing line lands inside it. Any resolution that re-emits the field rather than editing the surviving one produces a duplicate key. That is a mechanical trap, not a lapse of attention, and it is exactly what a gate is for.

Impact when it does ship

The advertised release becomes parser-dependent: last-wins readers resolve the newer version, first-wins readers retain the stale one, and duplicate-rejecting consumers can reject the manifest outright — so a plugin update can be misidentified or blocked.

Which gate should own the check, and why

The hygiene job, adjacent to its existing Validate plugin manifests step.

  • It already enumerates exactly the right file set (plugins/*/.claude-plugin/plugin.json) for schema validation, so the glob and the ownership already exist there — no new file-discovery logic is needed.
  • The defect class is document-level JSON integrity, the same tier as "does it parse" and "does it match the schema." That is hygiene's concern; plugin-gate owns manifest semantics (contracts, catalog parity), which is a different tier.
  • Most importantly, the schema step is the check a reader would assume already covers this. Putting the duplicate-key check next to it removes the false assurance at the point where the assurance is currently implied, rather than burying it in a semantics job.
  • Wiring is cheap: the job's step outcomes are already aggregated by scripts/aggregate-hygiene-results.sh, where any non-success outcome fails the job. It is one step plus one line in the checks block.

Not implemented here, per direction. Two notes for whoever picks it up:

  • The detector is cheap and needs no new toolchain: Python's json.load(..., object_pairs_hook=...) sees the raw key/value pairs before de-duplication, and Python is already present in that job because check-jsonschema is a Python tool.
  • Worth scoping to .claude-plugin/marketplace.json as well as the plugin manifests — same defect class, same conflict shape, same blast radius.

Related

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

agent-readyFully specified and briefed; eligible for autonomous pickup from the frontier.priority: highSignificant impact, or blocks an imminent release; staff this cycle.status: readyTriaged, unblocked, and fully specified; eligible to pick up.work-class: scopedA briefed fix or small feature; blast radius bounded by the brief, tests exist.

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions