SuperAPI AI Directory

plugin-parity-drift

Coding & development

View repository

Updated 10-15 20:13

About this skill

Plugin Parity Drift Detector

Lisa reimplements some curated third-party Claude plugins as Lisa-native skills so the behavior reaches agents that can't load the Claude plugin (Codex, agy, Copilot). Each such skill is version-pinned to the upstream plugin it mirrors. When upstream advances, the Lisa reimplementation silently falls behind. This skill detects that drift deterministically.

It is the empirical core of the parity subsystem (issue #1059) and is designed to run in CI. The sibling skills are analyze-plugin (plans parity work) and implement-plugin-parity (executes an approved plan, including stamping the synced-from pins this detector reads).

The synced-from pin grammar

A Lisa-native skill that reimplements an upstream plugin carries a synced-from pin in addition to the frontmatter every skill requires (name, description, allowed-tools; note $ARGUMENTS is not substituted in skills):

synced-from: <name>@<marketplace>@<version>

Examples:

synced-from: code-simplifier@claude-plugins-official@1.0.0
synced-from: coderabbit@claude-plugins-official@1.1.1

Grammar (EBNF):

synced-from   = plugin-ref "@" version
plugin-ref    = name "@" marketplace      ; the canonical Claude plugin id, exactly as in enabledPlugins
name          = 1*( ALPHA / DIGIT / "-" / "_" )
marketplace   = 1*( ALPHA / DIGIT / "-" / "_" )
version       = semver                    ; MAJOR.MINOR.PATCH[-prerelease][+build], semver 2.0.0

Parse rule: semver never contains @, so the value is split on the last @ (right side = version, remainder = plugin-ref), then the plugin-ref is split once more on its single @. The plugin-ref is byte-identical to the enabledPlugins key and to the cache path <marketplace>/<name>/, so it doubles as the resolver lookup key. A malformed value is reported unparseable.

How "current upstream" is resolved (deterministic)

For each plugin-ref = name@marketplace, the detector resolves the current version purely from the installed-plugin cache tree — no network, no Date, no installed_plugins.json:

cacheRoot/<marketplace>/<name>/<version-subdir>/.claude-plugin/plugin.json

It reads each subdir's manifest version field (NOT the directory name), collects the valid semver values, and picks the max by semver precedence. Non-semver dirs (unknown, git hashes) are skipped because the manifest version is authoritative. "Highest installed version" is the machine-independent definition of current upstream.

Running it

node scripts/plugin-parity-drift.mjs [--skills-root <dir>]... [--cache-root <dir>] [--json]
  • --skills-root <dir> (repeatable; default <repo>/.claude/skills): roots scanned recursively for **/SKILL.md.
  • --cache-root <dir> (default $CLAUDE_PLUGIN_CACHE, else ~/.claude/plugins/cache): the installed-plugin cache root.
  • --json: emit the machine-readable report (§3.5 of the design) instead of the human markdown table. The exit code is identical in both modes.

Default invocation (what CI runs):

node scripts/plugin-parity-drift.mjs

Per-skill statuses

Condition status drift?
current == pinned ok no
current > pinned stale yes
current < pinned ahead yes
plugin not in the cache not-installed yes
installed but no parseable semver unresolved yes
synced-from value malformed unparseable yes

Exit codes (CI contract)

  • 0 — no drift: every synced skill is ok.
  • 1 — drift found: ≥1 skill is stale / ahead / not-installed / unresolved / unparseable.
  • 2 — operational/usage error: unknown flag, missing --cache-root path, or no resolvable --skills-root. Distinct from drift so CI can tell a real drift from a broken invocation.

CI usage

Add a CI step that fails the build on exit 1:

- name: Plugin parity drift
  run: node scripts/plugin-parity-drift.mjs

The CI step needs the installed plugin cache present (default ~/.claude/plugins/cache, or set CLAUDE_PLUGIN_CACHE / pass --cache-root) so that synced skills can be resolved. Exception: a repo with zero reimplementations (no synced-from skills) passes cleanly with exit 0 even when the cache is absent — the detector scans skills first and short-circuits to "no drift" before requiring the cache, so a fresh runner without the cache never fails the build for a project that has nothing to track.

A stale result is the signal to run analyze-plugin for that plugin and then implement-plugin-parity to refresh the reimplementation and re-stamp the pin.

What it never does

The detector is report-only. It never edits a skill, never bumps a pin, and never touches the cache. Re-pinning is a deliberate human/implement-plugin-parity action so that "we re-reviewed the upstream change" is always an explicit step, never an automatic one.

Verifying the detector itself

The committed fixture under parity/fixtures/drift/ plus tests/unit/scripts/plugin-parity-drift.test.ts (run via bun run test:unit) prove the stale/ok/ignored classification, the max-semver resolution across sibling version dirs, the exit-code contract, and that the detector leaves the scanned skill bytes unchanged.