feat: consumable watch manifest + truecopy check-manifest (0.9.0)#71
Conversation
The weekly marketplace watch now publishes directory-manifest.json on the watch branch: name -> skill hash for everything it scanned, plus the currently-flagged names. New CLI command `check-manifest <file>` compares every INSTALLED marketplace plugin skill against it: - bytes differ from what the watch scanned -> drifted (exit 1) - watch-flagged -> fails even byte-identical (match is not endorsement) - unknown to the manifest -> unlisted (reported, never fatal) - manifest skills not installed here are ignored Offline like everything else: you fetch the manifest, truecopy reads it. Prototype-safe manifest lookups (Object.hasOwn; a skill named after a prototype member cannot read an inherited "expected hash"). Verified live on this machine: 29 installed official-directory plugin skills vs a manifest generated from the directory at tip -> all match, exit 0. Install-vs-corpus hash parity proven before building (installed marketplace trees hash byte-identical to the directory clone). 0.9.0: this plus the per-file acceptance internals (#69) ship to npm on merge via the usual auto-release path.
sprayberry-reviewer
left a comment
There was a problem hiding this comment.
Automated review from the Sprayberry Labs fleet code reviewer.
Verdict: No blocking issues found — clean, well-reused, well-tested addition. Approving, with one minor edge-case note below.
What I checked
gh pr diffin full (8 files, +191/-5).- Supporting context:
src/claude.mjs(discoverClaudePluginSkills,collectPluginSkills),src/index.mjs/src/skill.mjs(loadSkill,skillHash),support/marketplace-watch.mjsmanifest emission. - CI rollup (
gh pr checks 71):test (ubuntu/macos, 20/22)andverify pinned skillsall pass;test (windows-latest, 20/22)andanalyze (javascript-typescript)were still pending at review time — no failures observed, so I did not block on them per the "review the diff on its own merits" guidance.
Findings
Minor (non-blocking): src/cli.mjs:282 — the expected field is only attached to the result row when it's truthy:
row = { name, marketplace, status, hash, ...(expected && expected !== hash ? { expected } : {}) };
If a manifest entry's hash value is the empty string "" (a malformed/hostile manifest — the exact threat model this function's Object.hasOwn guard a few lines up is defending against), expected is falsy, so status still computes to 'drifted' (line 281: expected === hash ? 'match' : 'drifted', and '' !== hash) but the expected key is dropped from row. The non-JSON render path then does r.expected.slice(0, 12) at src/cli.mjs:294, which throws on undefined. Practically this still exits 1 via the outer try/catch in cli.mjs (fail-closed is preserved, and marketplace-watch.mjs's own skillHash output is never empty, so this needs an adversarial/malformed manifest to trigger) — so I'm not blocking on it, but a String(expected ?? '')-style guard or including expected whenever status === 'drifted' would make the render path robust to the same hostile-manifest input the Object.hasOwn check already anticipates.
row = { name, marketplace, status, hash, ...(status === 'drifted' ? { expected } : {}) };
What's good
check-manifestreusesdiscoverClaudePluginSkills,loadSkill,skillHashrather than reimplementing discovery/hashing — right layer, no duplication.- The
Object.hasOwnguard against a hostile manifest carrying__proto__/constructor-shaped keys is deliberate and has a dedicated test (toolkit:helpervsconstructor:skillintest/check-manifest.test.mjs). flaggedis checked independently of hash equality, so a byte-identical install of a watch-flagged skill still fails — correctly matches the stated "match is not endorsement" invariant, and is covered by its own test.- Shape validation on the manifest (
skillsmust be a plain object map, not array/missing) exits 2 distinctly from a real drift/flag (exit 1) — good error-code hygiene, and tested. marketplace-watch.mjschanges are minimal and additive:manifestSkillsbuilt alongside the existing scan loop, written as a newdirectory-manifest.jsonartifact without touching existingresults.json/badge.jsonoutput; existing tests extended in-place to assert the new artifact's shape.
Turns the weekly watch from a badge into a dependency: our standing vetting of the official Claude Code plugin directory becomes a drop-in check for any machine that installs from it. First real adoption hook for the watch.
What ships
directory-manifest.jsonon the watch branch — each watch run now publishes name → skill hash for everything it scanned, plus the currently-flagged names. The publish step needs no workflow change (git add -Aon the out dir already carries it).truecopy check-manifest <file>— compares every installed marketplace plugin skill against the manifest:drifted, exit 1 — what's on disk is not what was scannedflagged, exit 1 even byte-identical — a hash match is not an endorsementunlisted, reported but never fatal (your own plugins / other marketplaces are normal)Offline by design: you fetch the manifest, truecopy only reads it. Manifest lookups are prototype-safe (
Object.hasOwn— a skill namedconstructor:skillcan't read an inherited "expected hash"; there's a test for exactly that).Verification
hookify:writing-rules,frontend-design:frontend-design— MATCH on both), and CC's sync materializes LF-clean on Windows, so the comparison is honest cross-OS.check-manifestagainst the real~/.claude→ 29 installed plugin skills, allmatch, exit 0.Release
Bumps 0.9.0 — merging fires auto-release → npm via the usual tokenless OIDC path. CHANGELOG covers this plus the #69 per-file-acceptance internals (
scanPieces), which ship in the same version.