Skip to content

feat(scanner): derive skill/plugin source from install manifests#90

Open
SherlockSalvatore wants to merge 3 commits into
RealZST:mainfrom
SherlockSalvatore:feat/manifest-source-resolution
Open

feat(scanner): derive skill/plugin source from install manifests#90
SherlockSalvatore wants to merge 3 commits into
RealZST:mainfrom
SherlockSalvatore:feat/manifest-source-resolution

Conversation

@SherlockSalvatore

@SherlockSalvatore SherlockSalvatore commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

Closes #89.

Problem

scanner::detect_source infers an extension's source by walking up to the nearest enclosing .git. When an agent home (e.g. ~/.claude) is kept under the user's own dotfiles repo, everything beneath it — plugins cached in ~/.claude/plugins/cache/<marketplace>/..., skills, etc. — is mis-attributed to that backup remote, collapsing many unrelated sources into one bogus group. PR #88 fixed only the symlinked-skill slice; this addresses the root cause for skills and plugins.

Fix

Read each tool's own install manifest as the authoritative source; fall back to detect_source only when there is no manifest entry.

  • Skills — override with <root>/.skill-lock.json (the skills CLI lockfile), matched by the on-disk folder name (how the CLI keys it), parsed once per lockfile and cached. A real git checkout's commit hash is preserved.
  • PluginsPluginEntry gains source_url; the Claude adapter fills it from plugins/known_marketplaces.json (github marketplaces → owner/repo); scan_plugins prefers it over the .git walk. Non-github marketplaces are skipped so we never fabricate a wrong URL. Other adapters pass None (behaviour unchanged).

Testing

  • cargo test -p hk-core — 531 passed, incl. two new regression tests:
    • test_skill_lock_overrides_enclosing_git_source — the lockfile beats a populated enclosing repo; a sibling skill absent from the lock still falls back; the override matches by folder name even when frontmatter name differs.
    • test_scan_plugins_attributes_to_marketplace_repo — a plugin is attributed to its marketplace's upstream repo.
  • cargo clippy -p hk-core — clean for the touched files.

Notes

Store self-heal (existing DBs)

The scanner change fixes attribution going forward, but on an existing DB the git-source backfill had already stamped install_url/pack from the old (wrong) source, and the backfill only runs on install_type IS NULL so it never refreshes them — and deriveExtensionUrl prefers install_url, so the corrected source_json.url stays shadowed.

refresh_stale_git_install_meta (parallel to #88's self-heal) realigns install_type='git' skill/plugin rows whose recorded owner/repo differs from the now-authoritative source_json.url owner/repo, clearing the stale branch/subpath + pack (re-derived by backfill_packs). It compares by pack, not raw URL string, so a legitimate install recorded as …/repo is not churned against the scanner's …/repo.git remote every sync (which would wipe its pinned revision/check state). Verified on a real polluted DB: 13 plugins re-attributed to their marketplaces, same-repo and self-authored skills left untouched.

… repo

A skill installed into the shared ~/.agents/skills and symlinked into an
agent home (e.g. ~/.claude/skills) was mis-attributed when that home sits
inside a dotfiles git repo: detect_source walked the symlink's textual
parents, hit ~/.claude/.git, and recorded the dotfiles repo as the source.
The git-source backfill then stamped install_type='git' + pack, forking one
on-disk skill into two Extension rows (marketplace vs dotfiles).

- scanner: canonicalize the skill path before detect_source so a symlinked
  skill is attributed to its real content's source; plain skills resolve to
  the same tree and are unchanged.
- store: self-heal existing DBs by clearing the bogus git install_meta (and
  the pack derived from it) for skill rows whose on-disk entry is a symlink
  and whose freshly-scanned source is non-git; real git installs are plain
  files, never symlinks, so they are untouched. Run before backfill_packs so
  cleared rows do not re-acquire a pack.

Fixes RealZST#87
HarnessKit inferred an extension's source by walking up to the nearest
enclosing .git, mis-attributing everything under a dotfiles-managed agent
home (e.g. ~/.claude) to that backup repo. Read each tool's own install
manifest instead, falling back to .git detection only when absent.

- Skills: override detect_source with <root>/.skill-lock.json (the skills
  CLI lockfile), matched by on-disk folder name, cached per lockfile.
- Plugins: PluginEntry gains source_url; the Claude adapter fills it from
  plugins/known_marketplaces.json (github marketplaces -> owner/repo);
  scan_plugins prefers it over the .git walk. Other adapters unchanged.

Regression tests: lockfile beats a populated enclosing repo (and a sibling
skill absent from the lock falls back); a plugin is attributed to its
marketplace repo.

Refs RealZST#89. Builds on RealZST#88 canonicalize in scan_skill_dir.
The scanner now resolves the real source from install manifests, but the
git-source backfill only writes install_meta when install_type IS NULL, so a
row stamped in an earlier sync (e.g. a plugin first attributed to the
enclosing dotfiles repo) keeps its stale install_url. deriveExtensionUrl
prefers install_url, so the corrected source_json.url stayed shadowed and the
extension lingered in the wrong group.

Add refresh_stale_git_install_meta: for install_type='git' skill/plugin rows
whose install_url owner/repo differs from the authoritative source_json.url
owner/repo, realign install_url/revision and clear the now-stale
branch/subpath + pack (re-derived by backfill_packs). Compare by pack, not raw
URL string, so a legitimate install recorded as ".../repo" is not churned
against the scanner's ".../repo.git" remote every sync (which would wipe its
pinned revision and check state). Runs in both sync paths after the symlink
heal, before backfill_packs. This is the store half of the manifest
source-resolution fix (parallel to the RealZST#88 self-heal).
@SherlockSalvatore SherlockSalvatore force-pushed the feat/manifest-source-resolution branch from 61c6665 to d6d3b8d Compare June 24, 2026 09:00
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.

Derive extension source from the tool's install manifest, not the enclosing .git

1 participant