Skip to content

fix(cli): resolve bundled skills by proximity, not candidate order - #1007

Open
shashank-100 wants to merge 3 commits into
modem-dev:mainfrom
shashank-100:fix/bundled-skill-resolution-proximity
Open

fix(cli): resolve bundled skills by proximity, not candidate order#1007
shashank-100 wants to merge 3 commits into
modem-dev:mainfrom
shashank-100:fix/bundled-skill-resolution-proximity

Conversation

@shashank-100

Copy link
Copy Markdown

Problem

resolveBundledSkillPath nests its search loops candidate-outer, ancestor-inner, so each candidate shape is walked to the filesystem root before the next shape is tried at all:

for (const root of roots) {
  for (const relativePath of relativeCandidates) {   // candidate is the outer walk
    const resolvedPath = findRelativePathFromAncestors(root, relativePath);

With root = process.execPath (~/.local/bin/hunk), the generic skills/<name>/SKILL.md shape is tested at ~/.local/bin, ~/.local, ~, /home, / — all before hunkdiff/skills/<name>/SKILL.md is tried at ~/.local/bin, where scripts/install-bin.ts actually stages the skills for a source install.

User impact: anyone with an unrelated ~/skills/hunk-review/SKILL.md — a personal skills folder, or a Hunk checkout at ~ — runs bun run install:bin and then gets that unrelated file from hunk skill path, not the one just installed.

The hunkdiff/ nesting exists precisely to avoid claiming generic skill names beside the executable (see the comment at scripts/install-bin.ts:46-47); the resolver's loop order defeated that intent.

Approach

Invert the nesting so every candidate shape is tested at one ancestor before the walk moves up — proximity wins over candidate order — and order the candidates most-specific-first so a directory holding several layouts resolves to the one that names Hunk explicitly.

findRelativePathFromAncestors now takes the candidate list rather than a single path. It has one caller, so no other resolution behavior changes.

Non-goals: the set of supported layouts is unchanged, and no discovery root is added or removed. This only fixes which of several existing matches wins.

Why core

Skill resolution is CLI startup behavior in core/run, not renderer or extension surface.

Tests

Added two regression tests in packages/hunk/src/core/run/paths.test.ts:

  • a staged skill beside the binary beats a generic skills/ directory further up (the reported bug);
  • the packaged node_modules/hunkdiff/ layout beats a generic skills/ sibling at the same ancestor.

Both were confirmed to fail on the unfixed resolver and pass with the fix, so they guard the behavior rather than describe it. This path previously had no coverage for a competing ancestor.

Commands run

bun run typecheck    # clean
bun run lint         # 0 warnings, 0 errors
bun run deps:check   # no dependency violations (568 modules, 1893 dependencies)
bun run test         # 3847 pass, 47 skip, 2 fail
bun test packages/hunk/src/core/run/paths.test.ts   # 11 pass, 0 fail

The 2 failures are in test/cli/install-vm/contract.test.ts and are pre-existing on main, not caused by this change. Reproduced on a clean tree at upstream/main:

$ git stash push -m "verify preexisting failures"
$ bun test test/cli/install-vm/contract.test.ts
 10 pass
 1 fail
(fail) install VM contract > allows cleaning only real harness-owned paths and rejects symlink ancestors
error: Refusing install VM path outside /private/var/folders/.../tmp/install-vm: /var/folders/.../tmp/install-vm/cache

The harness compares a realpathSync-resolved root against an unresolved target, so macOS's /var/private/var symlink makes the containment check fail. Unrelated to skill resolution.

Platforms

Verified on macOS (darwin 25.6.0). Not run on Linux or Windows; the change is node:path/node:fs only and adds no platform assumptions, and the new tests build their fixtures with path.join rather than literal separators.

Follow-ups

Not addressed here: scripts/changeset-version.ts:54 unconditionally deletes the staged package changelog in a finally, so a failed write-back on line 53 destroys the freshly generated changelog after the .changeset/*.md files have already been consumed. Narrow, but recovery is manual and it runs during releases.

🤖 Generated with Claude Code

https://claude.ai/code/session_019jAgP7j1gaT9fSk5syLHDH

`resolveBundledSkillPath` walked each candidate shape to the filesystem
root before trying the next one, so the generic `skills/<name>/SKILL.md`
shape was tested at every ancestor before `hunkdiff/skills/<name>/SKILL.md`
was tried at the install directory — which is exactly where
`scripts/install-bin.ts` stages skills for a source install.

A user with an unrelated `~/skills/hunk-review/SKILL.md` therefore got that
file instead of the installed one. The `hunkdiff/` nesting exists to avoid
claiming generic skill names beside the executable; the resolver's loop
order defeated that intent.

Test every candidate shape at one ancestor before moving up, and order the
candidates most-specific-first so a directory holding several layouts
resolves to the one that names Hunk explicitly.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019jAgP7j1gaT9fSk5syLHDH
@greptile-apps

greptile-apps Bot commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

PR author is not in the allowed authors list.

@vercel

vercel Bot commented Sep 6, 2026

Copy link
Copy Markdown

@shashank-100 is attempting to deploy a commit to the Modem Team on Vercel.

A member of the Team first needs to authorize it.

CONTRIBUTING requires non-empty Changeset summaries to be a single
user-facing sentence; the original had two and the second described the
implementation rather than the user-visible effect.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019jAgP7j1gaT9fSk5syLHDH
@benvinegar

Copy link
Copy Markdown
Member

Nice fix. The ancestor-first walk looks right. One small tweak: please prefer hunkdiff/skills, then skills, then node_modules/hunkdiff/skills at the same directory. Otherwise a standalone build can pick a stale node_modules skill instead of the copy shipped beside the binary. The second test should cover that case.

Order the candidate shapes within one directory as `hunkdiff/skills`, then
`skills`, then `node_modules/hunkdiff/skills`. A standalone build ships its
skills beside the executable, so that copy should win over a
`node_modules/hunkdiff` belonging to whatever project happens to share the
directory, which may be pinned to another version and left stale.

Rework the specificity test to cover it: all three shapes at one ancestor,
asserting the shipped copy resolves.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019jAgP7j1gaT9fSk5syLHDH
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