Skip to content

fix(skills): resolve skill_manage targets through the symlink-following index walk - #17

Merged
yoyodine-industries merged 1 commit into
mainfrom
fix/skill-manage-symlink-index-walk
Sep 15, 2026
Merged

yoyodine-industries merged 1 commit into
mainfrom
fix/skill-manage-symlink-index-walk

Conversation

@yoyodine-industries

Copy link
Copy Markdown
Owner

Concern

skill_manage refuses every write in a profile that installs each skill as a
per-skill symlink (the curated lanes: yoyodine-coder, yoyodine-financially,
yoyodine-research), while the read path resolves the very same skill fine.

$ skill_manage(action='patch', name='yaan-citation-integrity', ...)
Skill 'yaan-citation-integrity' not found in active profile 'yoyodine-financially'
- skills by that name exist in other profiles: 'default', 'yoyodine-majordomo'

Root cause (measured, not inferred)

tools/skill_manager_tool.py: _iter_skill_dirswalked the skills root withPath.rglob("SKILL.md"). rglob` does not descend into symlinked directories,
and in a curated lane each skill is a symlink into the shared tree:

profiles/yoyodine-financially/skills/yoyodine/yaan-citation-integrity
  -> ~/.hermes/skills/yoyodine/yaan-citation-integrity

so none of that lane's skills were reachable by the write path. Profiles whose
whole skills root is a symlink (default at ~/.hermes/skills,
yoyodine-majordomo -> ~/.hermes/skills) work, because rglob traverses the
root it is handed and everything inside is a real directory. That asymmetry is
exactly why the error message names default and yoyodine-majordomo as the
profiles that "have" the skill.

Measured on a live install (2026-09-15):

rglob over profiles/yoyodine-financially/skills -> 0 hits for yaan-citation-integrity
rglob over profiles/yoyodine-majordomo/skills   -> 1 hit (root is a symlink)
rglob over ~/.hermes/skills                     -> 1 hit
(entry/SKILL.md).exists()                       -> True   (reachable directly)
entry in set(rglob)                             -> False  (unreachable to the walk)

Fix

Route the walk through iter_skill_index_files, the same symlink-following
(followlinks=True, support/excluded dirs pruned) iterator the read path already
uses, and accept either the lexical or the resolved form of the skills root when
deciding whether a hit belongs to the local root. Both the active-profile finder
and the other-profile search share _iter_skill_dirs, so this one change fixes
both, and the cross-profile guard behaviour is unchanged.

Tests

New TestSymlinkedSkillDirs in tests/tools/test_skill_manager_tool.py covers a
per-skill symlink, a symlinked category directory, patch / write_file /
remove_file, the dispatcher patch path, and a symlinked whole skills root.

Red -> green, production file reverted to the pre-fix commit with the tests left
at this branch (this branch's parent is the pre-fix tip, so the revert is exact):

fix/tip parent (8dbf220ae) production file, tests at this branch:
    5 failed, 5 passed   FAILED TestSymlinkedSkillDirs::test_patch_edits_canonical_file_behind_symlink
                         FAILED ...::test_write_file_lands_in_canonical_dir_behind_symlink
                         FAILED ...::test_dispatcher_patches_through_symlink
this branch:
    10 passed

No behaviour change elsewhere. Same-tree A/B on the skill-management surface
(tests/tools tests/agent -k skill: 972 collected here vs 966 on the base tree,
the delta being these tests): identical failure sets (4 pre-existing failures
in both trees, 967 passed here vs 961 on base). Focused guard-file A/B
(tests/tools/test_cross_profile_guard.py + tests/tools/test_skill_manager_tool.py):
79 passed here vs 73 on base, zero failures either side.

Scope

One concern: the skill_manage write path. The same rglob("SKILL.md") pattern
survives in ~14 other modules and several are user-visible in a curated lane
(hermes_cli/profiles.py per-profile skill count, hermes_cli/profile_describer.py
summary, hermes_cli/web_routers/profiles.py installed-skill names,
tui_gateway/methods_profiles.py listing, agent/skill_usage.py,
agent/curator_backup.py, agent/learning_graph.py, gateway/run.py hints) -
each needs its own semantics check and belongs in its own change, not here. It
is flagged on the card, not bundled into this diff.

Upstream

No upstream PR from this branch: NousResearch/hermes-agent issue NousResearch#35184
describes this exact defect and sibling PR NousResearch#67748 (fix/repo-backed-skill-resolution,
open) makes the same substitution, so upstream is a HOLD per the fork rule.

…ng index walk

`_iter_skill_dirs` walked the skills root with `Path.rglob("SKILL.md")`. rglob
does not descend into symlinked directories, so a profile that installs each
skill as a per-skill symlink into a shared tree had none of its skills visible
to the write path, while the read path (`skill_view` / `skills_list`, via
`iter_skill_index_files`) listed them all. Every write in such a profile failed
with "Skill '<name>' not found in active profile '<profile>'".

Route the walk through `iter_skill_index_files`, the same iterator the read path
uses, and accept either the lexical or the resolved form of the skills root when
deciding whether a hit belongs to the local root. Both finders (active profile,
and the other-profile search) share `_iter_skill_dirs`, so one change fixes both.

Regression tests cover a per-skill symlink, a symlinked category directory,
`patch` / `write_file` / `remove_file`, and the cross-profile lookup.
@github-actions

github-actions Bot commented Sep 15, 2026 •

Copy link
Copy Markdown

૮ >ﻌ< ა ci review

running on c07fbe5 — fix(skills): resolve skill_manage targets through the symlin


Still running 2 jobs: OS-specific tests / Windows-only tests, Python tests / Run tests

⚠️ Warnings

OSV vulnerability scan · View job

76 known vulnerabilities found in pinned dependencies.

How to fix:

Review the findings in the Security tab. Update the affected dependencies if a patched version is available.

@yoyodine-industries
yoyodine-industries merged commit b6a343d into main Sep 15, 2026
31 of 34 checks passed
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.

1 participant