Skip to content

fix(skill-quality): drop the duplicate version member from plugin.json - #1494

Merged
kyle-sexton merged 1 commit into
mainfrom
fix/skill-quality-duplicate-version-key
Jul 26, 2026
Merged

fix(skill-quality): drop the duplicate version member from plugin.json#1494
kyle-sexton merged 1 commit into
mainfrom
fix/skill-quality-duplicate-version-key

Conversation

@kyle-sexton

@kyle-sexton kyle-sexton commented Jul 26, 2026

Copy link
Copy Markdown
Contributor

Summary

plugins/skill-quality/.claude-plugin/plugin.json carried two version members on main:

4:  "version": "0.11.0",
5:  "version": "0.12.0",

The advertised release was therefore parser-dependent — last-wins readers (jq, Python json.load) resolve 0.12.0, first-wins readers retain 0.11.0, and duplicate-rejecting consumers can reject the manifest outright. Any of those can misidentify or block a plugin update.

Why 0.12.0 is correct. main carried 0.11.0 before #1096, released in #1450, and this plugin's CHANGELOG has ## [0.12.0] above main's ## [0.11.0]. The 0.11.0 member is the stale one, and it is what this PR deletes.

Scope. One file, one deletion. Nothing else in the manifest and nothing else in the repo.

How it happened. Introduced while resolving a main merge in #1096. Both sides of that conflict carried "version": "0.11.0" — main had released 0.11.0 in #1450 and the branch also claimed 0.11.0 — so git treated the version line as common context outside the conflict region and placed only the differing description inside it. A scripted replacement of the conflict region then emitted a fresh version + description pair above the surviving common-context one.

Test plan

A duplicate-key-aware parse is the only check that observes this, and it is clean after the change:

$ python -c "... json.load(..., object_pairs_hook=<duplicate detector>) ..."
duplicate keys: NONE
version: 0.12.0

Also confirmed:

  • git diff --stat is exactly 1 file changed, 1 deletion(-).
  • The CHANGELOG's top section is still ## [0.12.0], so the manifest and the release notes agree.
  • changelog-parity-gate and plugin-gate pass in CI on this branch.

Worth flagging for reviewers: json.load, jq, and JSON Schema validation all resolve duplicate keys last-wins, so every one of them reports 0.12.0 whether or not the duplicate is present. That is why this defect passed a fully green 27-context suite on #1096, and it is why the fix is verified with a duplicate-key hook rather than a plain parse.

Related

Closes #1492

The manifest carried two `version` members, 0.11.0 and 0.12.0, so the
advertised release was parser-dependent: last-wins readers resolve 0.12.0,
first-wins readers retain 0.11.0, and duplicate-rejecting consumers can
reject the manifest outright. Any of those can misidentify or block a
plugin update.

0.12.0 is correct. Main carried 0.11.0 before #1096 (released in #1450),
and this plugin's CHANGELOG has `## [0.12.0]` above main's `## [0.11.0]`,
so the 0.11.0 member is the stale one.

Introduced resolving a main merge in #1096: both sides of that conflict
carried 0.11.0, so git kept the version line as common context OUTSIDE the
conflict region and placed only the differing description inside it, and a
scripted replacement of that region emitted a fresh version member above
the surviving one.

Closes #1492
@claude

claude Bot commented Jul 26, 2026

Copy link
Copy Markdown
Contributor

Claude finished @kyle-sexton's task in 51s —— View job


Review

  • Read changed file
  • Verify diff and correctness
  • Check CHANGELOG alignment
  • Post findings

LGTM. The fix is correct, minimal, and well-reasoned.

Diff:

The deletion removes "version": "0.11.0" (the stale duplicate that appeared first in the object). The surviving entry is "version": "0.12.0" at line 4 of the post-fix file, which matches the current top-level entry in the CHANGELOG (## [0.12.0] at line 6, above ## [0.11.0] at line 70).

Correctness:

Scope: Single-file, single-deletion, no test or documentation changes needed — the manifest carries no executable logic.

Verification note: The PR body's observation that jq, json.load, and JSON Schema validation are last-wins resolvers (and therefore masked this defect) is correct. The duplicate-key-aware parse cited as the actual check is the appropriate gate. Tracking the missing CI gate separately (#1492 / mentioned in the body) is the right call for this PR.

No issues. Ready to merge.

@claude

claude Bot commented Jul 26, 2026

Copy link
Copy Markdown
Contributor

Claude finished @kyle-sexton's task in 35s —— View job


Security Review

  • Gather context and understand the request
  • Fetch the PR diff
  • Read changed file(s)
  • Analyze for security issues
  • Post findings

No security issues found.

The sole change is a one-line deletion from a static JSON manifest:

-  "version": "0.11.0",
   "version": "0.12.0",

plugins/skill-quality/.claude-plugin/plugin.json

There is nothing security-relevant to flag:

  • No code execution surface. The file is a declarative JSON manifest. No scripts, hooks, or executable paths are modified.
  • No secrets or credentials. No tokens, passwords, or sensitive values appear anywhere in the diff or in the post-change file.
  • No GitHub Actions changes. No workflow files are touched; no pull_request_target, workflow_run, or permissions: mutations.
  • No dependency or action pins loosened. No package versions or action refs changed.
  • No injection surface. The $schema reference (https://json.schemastore.org/claude-code-plugin-manifest.json) is unchanged and points to a well-known public registry.
  • No path traversal or plugin-isolation risk. The file sits inside the plugin's own directory tree; no ../ reach-outs are introduced.

The change is a correctness fix (removing a parser-dependent duplicate key) with no security footprint.

@kyle-sexton
kyle-sexton merged commit dcfa7c8 into main Jul 26, 2026
28 of 29 checks passed
@kyle-sexton
kyle-sexton deleted the fix/skill-quality-duplicate-version-key branch July 26, 2026 06:32
kyle-sexton added a commit that referenced this pull request Jul 26, 2026
…1506)

Closes #1498

## Summary

*This was generated by AI during work-loop execution.*

No gate in CI could detect a duplicate key in a plugin/marketplace
manifest. JSON Schema validates the already-parsed document
(structurally blind to a repeated member), and every consumer downstream
resolves it last-wins with no signal — the exact shape that shipped
`plugins/skill-quality/.claude-plugin/plugin.json` with two `"version"`
members through a fully green 27-context suite (#1492).

## Fix

Added `scripts/check-manifest-duplicate-keys.py`: reads each
`plugins/*/.claude-plugin/plugin.json` and
`.claude-plugin/marketplace.json` via `json.loads(text,
object_pairs_hook=...)`, intercepting the raw key/value pairs of every
JSON object literal (at any nesting depth) *before* the standard
de-duplicating collapse into a dict. A duplicate is recorded (not
raised) so the parse still completes last-wins — identical to how
`json.load`/`JSON.parse`/`jq` resolve it — and every distinct duplicate
key in a file is reported in one pass.

Wired into the `hygiene` job in `.github/workflows/ci.yml`, directly
adjacent to the existing "Validate plugin manifests" step (per the
issue's own routing rationale — same file scope, same tier as "does it
parse"/"does it match the schema"), gated the same way the four
`check-jsonschema` steps are (skipped only on a provably docs-only
diff), and its outcome is added as one line to
`scripts/aggregate-hygiene-results.sh`'s existing `CHECK_RESULTS` block
so a duplicate key fails the job exactly like any other hygiene check.

## Verification

- `bash scripts/check-manifest-duplicate-keys.test.sh` — 17 unit/CLI
tests, all passing (nested-object duplicates, sibling-object
non-duplicates, multiple distinct duplicate keys in one file, malformed
JSON *not* double-reported, default-discovery glob covering both
`plugin.json` and `marketplace.json`, missing-file handling, exit
codes).
- `test_catches_the_1492_shaped_duplicate_version_key` reproduces the
actual #1492 defect shape (two `"version"` members) as an inline fixture
and asserts the gate fails on it — the fixture proving the gate catches
a real duplicate key, per the issue's own ask.
- Live empirical cross-check against the two claims in the issue's
verification table, run locally against a copy of the real (now-fixed)
`plugins/skill-quality/.claude-plugin/plugin.json` with the duplicate
`"version"` member re-injected:
- `check-jsonschema --schemafile
https://json.schemastore.org/claude-code-plugin-manifest.json <file>` →
`ok -- validation done`, exit 0 (confirms schema validation is blind to
it).
- `python3 scripts/check-manifest-duplicate-keys.py <file>` → `DUPLICATE
KEY: ... defines 'version' more than once ...`, exit 1 (confirms the new
gate catches exactly what schema validation cannot).
- `python3 scripts/check-manifest-duplicate-keys.py` (default discovery,
no args) against the actual repo tree → `No duplicate JSON object keys
found in 62 manifest file(s).`, exit 0 — no false positives on the real
manifest set.
- `actionlint .github/workflows/ci.yml` — clean.
- `shellcheck --rcfile=.shellcheckrc
scripts/check-manifest-duplicate-keys.test.sh` — clean.
- YAML parse + step-order check of the modified `hygiene` job — new
steps land in the intended position, ids wired correctly into
`CHECK_RESULTS`.
- `typos --config _typos.toml` over the new/changed files — clean.
- No `plugins/*` manifest touched, so the per-plugin CHANGELOG-parity
gate does not apply to this change.

## Related

- #1492 — the shipped duplicate-key instance this gate would have
caught.
- #1494 — the manifest fix for #1492.
- #1096, #1450 — the version-collision conflict shape that produces this
defect class.

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
kyle-sexton added a commit that referenced this pull request Aug 9, 2026
…ing YAML comments (#2002)

Three stranded post-merge review findings on the `skill-quality` plugin,
plus the
repository-level doc contradiction filed alongside them. Reviews posted
after their PRs
merged were invisible to the merge gate, so these sat unread.

## Zero-padded integer overrides were parsed as octal

`require_positive_number`'s `^[0-9]+$` accepts a zero-padded value, but
bash arithmetic and
`printf %d` then read it in base 8. Reproduced three ways against
`origin/main` in a
detached worktree:

- `CHECK_SKILL_LISTING_BUDGET_CHARS=08` — `printf: 08: invalid octal
number`, the budget
  rendered as `0`, and the report still announced `OK` and exited 0.
- `CHECK_SKILL_LISTING_BUDGET_CHARS=0123` — budget silently became 83.
- `CHECK_SKILL_LISTING_MAX_DESC_CHARS=010` — entries capped at 8 instead
of the requested 10.

Accepted integer overrides are now forced to base 10 at the one place
the digits become a
number. All three `require_positive_number ... int` call sites are
covered. The ratio and
fraction overrides are deliberately left alone: `0.01` is the documented
default fraction
and must keep its leading zero, and both reach only `awk`, which has no
octal input.
`CHECK_SKILL_LISTING_CHARS_PER_TOKEN` is likewise untouched because it
never reaches `$(( ))`
or `%d`. No other script in this plugin takes a numeric environment
override, so there is no
sibling occurrence of the pattern left behind.

## A trailing YAML comment was measured as part of the listing scalars

`skill_frontmatter::field` returned the comment along with the value,
which also hid the
surrounding quotes from `strip_quotes` so the quoting was counted too. A
fixture with
commented `description` and `when_to_use` scalars measured 52 characters
against a true 15,
producing false overflow warnings and wrong contributor sizes.

The fix went into the shared helper rather than the reporter, because
`check-skill.sh`
reads the same fields through it — the per-skill entry cap (Check 2) and
the
trigger-preservation diff had the same defect. Stripping is quote-aware
and confined to the
plain/flow branch: inside a block scalar a `#` is content, such as a
markdown heading in a
`description: |` body.

`normalize_bool`'s own `sed` comment strip is now redundant and was
removed, along with the
comment asserting that comment-stripping is "never applied to
`description` /
`when_to_use`". It has exactly one caller and that caller sources its
value through
`skill_frontmatter::field`, so nothing loses the strip.

Blast radius was measured rather than predicted: every `SKILL.md` under
`plugins/` was run
through both the old and the new extractor and the measured
`description` / `when_to_use`
lengths are identical across the repo — no real skill's measurement
moves.

## The fresh-eyes rule contradicted the dispatch ladder

`docs/PLUGIN-PHILOSOPHY.md` stated the normative rule as requiring "a
named subagent" while
its own dispatch ladder makes a generic fresh-context subagent the
default and its
named-agent bar says a named agent is earned, not default. An author
following the rule
would create a named agent unnecessarily.

Resolved toward the ladder rather than hedged, because that is what the
repo actually
enforces: the delegation-wording detector in `check-skill.sh` accepts a
generic worker —
it matches `fresh-context` plus any of
agent/subagent/worker/advisor/reviewer/verifier/
dispatch/delegate — and never requires a named agent.
`docs/topics/fresh-eyes-checkpoint-audit/PLAN.md`
specified this same reconciliation ("generic or named"); it was applied
to the preceding
paragraph and missed on the rule sentence itself. The author-facing spec
`skills/check/reference/fresh-eyes-declarations.md` is doctrine-agnostic
and needed no change.

Folding this nine-word documentation clause into the plugin PR instead
of buying a second
full CI cycle is a deliberate, operator-approved deviation from
one-PR-per-plugin.

## Also verified, no change needed

The fourth finding in this batch reported two `version` members in
`plugins/skill-quality/.claude-plugin/plugin.json`. Already fixed on
main by `dcfa7c8b70`
(#1494); the manifest carries exactly one. Recorded here so the thread
resolves against
evidence rather than silence.

## Testing

Seven regression cases added to `check-listing-budget.test.sh` covering
each octal
manifestation, the decimal fraction that must keep working, the quoted
and plain comment
forms, a `#` that is content rather than a comment, and a `#` inside a
block scalar.

Gates run green from the worktree root: the listing-budget and
`check-skill` suites,
`run-plugin-tests.sh`, `check-changelog-parity.sh --check` /
`--check-bump` / `--check-order`,
`check-changed-skills.sh`, `check-shell-portability.sh` (+ its suite),
`check-contract-slice-prune.sh --check` / `--check-diff`,
`check-contract-clause-coverage.py`,
ShellCheck over the three changed scripts, markdownlint over the two
changed docs, and the
CI invocation `check-listing-budget.sh plugins/*/skills`.

## Related

Refs #1938 — the stranded-findings triage sweep these threads came from.

No linked issue

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

skill-quality: plugin.json ships two version members (0.11.0 and 0.12.0) — advertised release is parser-dependent

1 participant