Skip to content

fix(skills): symlink-aware, cycle-safe Hermes skill discovery - #151

Merged
jramos merged 1 commit into
mainfrom
fix/symlink-aware-skill-resolver
Jul 6, 2026
Merged

fix(skills): symlink-aware, cycle-safe Hermes skill discovery#151
jramos merged 1 commit into
mainfrom
fix/symlink-aware-skill-resolver

Conversation

@jramos

@jramos jramos commented Jul 6, 2026

Copy link
Copy Markdown
Owner

Resolves the symlink item of upstream NousResearch/hermes-agent-self-evolution#142 (the one genuinely-new find from the 2026-07-06 triage sweep; the other five #142 fixes are already-covered clusters).

Problem

HermesSkillSource discovered SKILL.md via Path.rglob("SKILL.md") at three sites. rglob refuses to descend into symlinked directories on Python <3.13 (the recurse_symlinks kwarg is 3.13-only). The standard Hermes layout symlinks user-installed skills into the framework tree, so those skills silently resolved "not found." Reproduced red on 3.13: the symlinked-directory discovery tests failed, while the symlinked-file case already worked (rglob matches symlinked files).

Fix (scoped to evolution/core/skill_sources.py)

One private helper _iter_skill_files(root) replaces all three rglob sites:

  • os.walk(followlinks=True) — portable across 3.10–3.13 (no recurse_symlinks).
  • Cycle guard: a (st_dev, st_ino) visited-set prunes a symlink pointing back to an ancestor → provably terminating (os.walk(followlinks=True) alone loops forever on a cycle).
  • Deterministic: per-level sort() → stable first-match-wins.
  • Parity: onerror/stat failures are debug-logged and skipped, matching the old silent skip. find_skill walks once, runs both passes over the materialized list.

Blast radius: only HermesSkillSource. The flat ClaudeCodeSkillSource/LocalDirSkillSource already follow symlinks via is_file()/iterdir() — left untouched, noted inline. No public API change.

Tests — tests/core/test_skill_sources.py (+9)

Real os.symlink temp trees with a skip-guard where symlinks aren't creatable:

  • Correctness C1–C5: symlinked skill dir, symlinked intermediate category, symlinked SKILL.md file, frontmatter-fallback across a symlink, mixed real+symlink listing.
  • Safety S1–S3: symlink cycle terminates (would hang under naive followlinks), unreadable dir skipped, dangling symlink no-raise.
  • Determinism D1: duplicate skill name resolves to the lexicographically-first branch, stably.

The symlinked-directory cases were red on the old rglob code; all green after.

Verification

  • uv run pytest tests/ -q -m 'not slow'1763 passed (+9), 0 failures.
  • ruff check clean.

Marks the #142 (partial) triage action item DONE (row + review-log entry + snapshot delta, in this PR).

HermesSkillSource resolved SKILL.md via Path.rglob('SKILL.md') at three
sites. rglob refuses to descend into symlinked directories on Python <3.13
(the recurse_symlinks kwarg is 3.13-only), so a Hermes layout that symlinks
user-installed skills into the framework tree silently resolved 'not found'.

Replace all three sites with one private _iter_skill_files(root) helper:
os.walk(followlinks=True) + a (st_dev, st_ino) visited-set cycle guard
(prunes a symlink pointing back to an ancestor -> provably terminating) +
per-level sort() for deterministic first-match-wins. onerror/stat failures
are debug-logged and skipped (parity with the old silent skip). find_skill
walks once and runs both passes over the materialized list.

Scope held to HermesSkillSource: the flat ClaudeCode/LocalDir sources
already follow symlinks via is_file()/iterdir() (noted inline).

9 new symlink tests (correctness C1-C5, cycle/permission/dangling safety
S1-S3, determinism D1) with an os.symlink-unavailable skip-guard; the
symlinked-directory cases were red on the old rglob code. Full non-slow
suite green (1763 passed, +9), ruff clean.

Resolves the symlink item of NousResearch#142;
the other five #142 fixes are already-covered clusters. Marks the triage
action item DONE (row + review-log + snapshot delta).
@jramos
jramos enabled auto-merge (squash) July 6, 2026 21:24
@jramos
jramos merged commit ffe684c into main Jul 6, 2026
5 checks passed
@jramos
jramos deleted the fix/symlink-aware-skill-resolver branch July 6, 2026 21:31
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