fix(skills): attribute symlinked skills to real source, not enclosing repo#88
Merged
RealZST merged 1 commit intoJun 25, 2026
Conversation
… 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
SherlockSalvatore
added a commit
to SherlockSalvatore/HarnessKit
that referenced
this pull request
Jun 24, 2026
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.
SherlockSalvatore
added a commit
to SherlockSalvatore/HarnessKit
that referenced
this pull request
Jun 24, 2026
…ource 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' rows whose install_url disagrees with the authoritative source_json.url, realign install_url/revision and clear pack so backfill_packs re-derives it. Rows already in agreement and non-git installs are untouched. 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
added a commit
to SherlockSalvatore/HarnessKit
that referenced
this pull request
Jun 24, 2026
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).
RealZST
pushed a commit
to SherlockSalvatore/HarnessKit
that referenced
this pull request
Jun 25, 2026
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.
RealZST
pushed a commit
to SherlockSalvatore/HarnessKit
that referenced
this pull request
Jun 25, 2026
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).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #87.
Problem
When an agent home (e.g.
~/.claude) is kept inside a personal dotfiles git repo, a skill installed into the shared~/.agents/skills/<name>location and symlinked into~/.claude/skills/<name>is mis-attributed. The scanner walks the symlink's textual parents, hits~/.claude/.git, and records the dotfiles repo as the skill's source. The git-source backfill instore.rsthen stamps that row withinstall_type='git'+ the dotfilesurl/pack.Result: one on-disk skill (one path is literally a symlink to the other) appears as two rows in Extensions — one correctly sourced from the marketplace repo (Codex/Gemini/Copilot, which scan
~/.agents/skillsdirectly) and one falsely sourced from the user's dotfiles repo (Claude, via the symlink). The two can't be updated together.Fix
canonicalizethe skill path beforedetect_source, so a symlinked skill is attributed to its real content's source. Plain (non-symlinked) skills resolve to the same tree, so detection is unchanged.gitinstall_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're untouched. Runs beforebackfill_packsso cleared rows don't re-acquire a pack.After both, the symlinked Claude row scans as sourceless and re-attaches to its marketplace sibling via the existing
urlSiblingsgrouping pass → one row, all agents, updatable together.Testing
cargo test -p hk-core— 529 passed, incl. two new regression tests:scanner::test_symlinked_skill_attributed_to_real_source_not_enclosing_repostore::test_sync_heals_symlinked_git_install_meta— covers active + disabled symlinked skills (healed) and both a plain-file git install and a git-backed symlink (preserved).cargo clippy -p hk-core— clean for the touched files.Known limitations (out of scope)
~/.agentstree is itself a git repo, all agents canonicalize to the same git source and group into a single (correctly unified, though dotfiles-labeled) row — not a fork. Resolving the marketplace source there would require reading theskillsCLI lockfile, which HarnessKit does not consume today.sync_extensions_for_agentheals only the agent's current scan batch; a stale row for another agent is healed on that agent's next sync.Scope & follow-up
This PR fixes the symlinked-skill slice of a broader source-attribution family. They share one root cause:
scanner::detect_sourceinfers an extension's source by walking up to the nearest enclosing.git, which is wrong whenever the files live inside the user's own dotfiles repo (e.g.~/.claude/.git). Other faces of the same bug are not addressed here and will be fixed at the root in a follow-up:~/.claude/plugins/cache/<owner>/<repo>/are real directories inside the dotfiles repo, so they are all mis-attributed to it (their real marketplace is recorded in Claude's owninstalled_plugins.json). Symlink resolution can't help here — there is no symlink.~/.agentskept under git, or a marketplace skill copied as a real directory into~/.claude, hit the same enclosing-.gitinference.The intended root fix is to derive tool-managed extensions' source from each tool's install manifest (
~/.agents/.skill-lock.jsonfor skills,~/.claude/plugins/installed_plugins.jsonfor plugins) and fall back todetect_sourceonly for genuinely git-cloned extensions. That work is tracked separately; this PR is the immediate, low-risk fix for the symlinked-skill case and stays intentionally narrow.