Skip to content

feat: consumable watch manifest + truecopy check-manifest (0.9.0)#71

Merged
askalf merged 2 commits into
masterfrom
feat/directory-manifest
Jul 17, 2026
Merged

feat: consumable watch manifest + truecopy check-manifest (0.9.0)#71
askalf merged 2 commits into
masterfrom
feat/directory-manifest

Conversation

@askalf

@askalf askalf commented Jul 17, 2026

Copy link
Copy Markdown
Owner

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.json on 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 -A on the out dir already carries it).

truecopy check-manifest <file> — compares every installed marketplace plugin skill against the manifest:

curl -fsSLo directory-manifest.json https://raw.githubusercontent.com/askalf/truecopy/watch/directory-manifest.json
truecopy check-manifest directory-manifest.json
  • installed bytes ≠ watched bytes → drifted, exit 1 — what's on disk is not what was scanned
  • watch-flagged name → flagged, exit 1 even byte-identical — a hash match is not an endorsement
  • unknown to the manifest → unlisted, reported but never fatal (your own plugins / other marketplaces are normal)
  • manifest skills not installed here are ignored — the check is about what can run on this machine

Offline by design: you fetch the manifest, truecopy only reads it. Manifest lookups are prototype-safe (Object.hasOwn — a skill named constructor:skill can't read an inherited "expected hash"; there's a test for exactly that).

Verification

  • Parity proven before building: installed marketplace plugin trees hash byte-identical to the directory clone (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.
  • Live end-to-end on this machine: manifest generated from the directory at tip → check-manifest against the real ~/.claude29 installed plugin skills, all match, exit 0.
  • Suite: 150 tests, 0 fail (4 new for check-manifest: match/unlisted, drift, flagged-not-endorsed, fail-closed manifest shapes; watch tests assert the manifest artifact + flagged array).

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.

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 sprayberry-reviewer left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 diff in full (8 files, +191/-5).
  • Supporting context: src/claude.mjs (discoverClaudePluginSkills, collectPluginSkills), src/index.mjs/src/skill.mjs (loadSkill, skillHash), support/marketplace-watch.mjs manifest emission.
  • CI rollup (gh pr checks 71): test (ubuntu/macos, 20/22) and verify pinned skills all pass; test (windows-latest, 20/22) and analyze (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-manifest reuses discoverClaudePluginSkills, loadSkill, skillHash rather than reimplementing discovery/hashing — right layer, no duplication.
  • The Object.hasOwn guard against a hostile manifest carrying __proto__/constructor-shaped keys is deliberate and has a dedicated test (toolkit:helper vs constructor:skill in test/check-manifest.test.mjs).
  • flagged is 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 (skills must 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.mjs changes are minimal and additive: manifestSkills built alongside the existing scan loop, written as a new directory-manifest.json artifact without touching existing results.json/badge.json output; existing tests extended in-place to assert the new artifact's shape.

@askalf
askalf merged commit 134cf08 into master Jul 17, 2026
9 checks passed
@askalf
askalf deleted the feat/directory-manifest branch July 17, 2026 22:42
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.

2 participants