fix(skills): scan slash-command skills from the live profile, not import time - #326
Merged
Merged
Conversation
…ort time `agent/skill_commands.py` resolved the skills directory through `tools.skills_tool.SKILLS_DIR`, a module-level constant evaluated at import (`SKILLS_DIR = HERMES_HOME / "skills"`). In a long-lived backend serving more than one profile that constant is whatever home the process started under, so it goes stale the moment a request is scoped elsewhere. `skills_tool` already solved this for its own scan loop: `_skills_dir()` resolves the live profile `HERMES_HOME` on every call while still honoring an explicit `SKILLS_DIR` monkeypatch, and `skills_tool.skills_list` was migrated to it with the comment "the module-level SKILLS_DIR can be stale in long-lived runtimes". `skill_commands` was left behind — an unfinished migration, not a deliberate launch-home scope. There is no `get_process_hermes_home()` here and no docstring claiming process scope, which is how the codebase marks the genuinely launch-scoped assets (dashboard themes, dashboard plugin manifests). The visible effect: `scan_skill_commands()` is the `/<skill-name>` vocabulary the gateway's `command.dispatch` resolves against, so a session under a non-launch profile saw its OWN skills mid-turn (the agent resolves via `_skills_dir()`) but the LAUNCH profile's `/<skill-name>` list. A skill installed in that profile was invisible as a slash command, and a name present in both homes dispatched the wrong copy. Move all three sites to `_skills_dir()`: - `scan_skill_commands` — the scan root, mirroring `skills_tool`'s loop. - `_load_skill_payload` — the fallback reconstruction of `skill_dir` from a relative path, which otherwise built a path into the wrong home. - `_build_skill_message` — `skill_dir.relative_to(...)` for the `skill_view` hint, which otherwise raised `ValueError` against a profile skill and silently degraded to the external-dir branch. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This was referenced Aug 2, 2026
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.
What does this PR do?
agent/skill_commands.pyresolved the skills directory throughtools.skills_tool.SKILLS_DIR— a module-level constant evaluated at import(
SKILLS_DIR = HERMES_HOME / "skills",skills_tool.py:143). In a long-livedbackend serving more than one profile that constant is whatever home the process
started under, so it goes stale the moment a request is scoped elsewhere.
skills_toolalready solved this for its own scan loop._skills_dir()resolvesthe live profile
HERMES_HOMEon every call while still honoring an explicitSKILLS_DIRmonkeypatch, andskills_listwas migrated to it with the comment"the module-level SKILLS_DIR can be stale in long-lived runtimes"
(
skills_tool.py:693-696).skill_commandswas simply left behind.This is an unfinished migration, not a deliberate launch-home scope. Where this
codebase genuinely wants launch-home scoping it says so explicitly — a
get_process_hermes_home()call plus a docstring explaining why (dashboard themes,dashboard plugin manifests). Neither is present here.
The visible effect.
scan_skill_commands()is the/<skill-name>vocabularythe gateway's
command.dispatchresolves against. A session under a non-launchprofile saw its own skills mid-turn — the agent resolves via
_skills_dir()—but the launch profile's
/<skill-name>list. So a skill installed in thatprofile was invisible as a slash command, and a skill name present in both homes
dispatched the wrong copy. Same session, two different answers to "which skills
exist", depending on which code path asked.
Related Issue
No filed issue — found while auditing
command.dispatchfor profile scoping, thesame audit that produced #323 and #324.
Type of Change
Changes Made
All three
SKILLS_DIRsites inagent/skill_commands.pymove to_skills_dir():scan_skill_commands(the scan root) — nowactive_skills_dir = _skills_dir(),mirroring the loop in
tools/skills_tool.py. This is the one that made profileskills invisible as slash commands.
_load_skill_payload— the fallback reconstruction ofskill_dirfrom arelative
skill_path, which otherwise built a path into the wrong home._build_skill_message—skill_dir.relative_to(...)for theskill_viewhint. Against a profile skill this raised
ValueErrorand silently fell into the"external dir" branch, producing a wrong
skill_viewtarget.corrected a now-inaccurate comment that still described the old reconstruction.
tests/tools/test_skills_tool_profile_scope.py— 3 new tests (see below). Addedto the existing profile-scope regression file so the shared
_write_skill/_reload_skills_toolhelpers and the concern stay in one place.The
SKILLS_DIRmonkeypatch escape hatch is preserved — 33 test files depend on it,and
_skills_dir()checks the patched value before resolving live. A new test pinsthat contract for this code path, alongside the existing
test_explicit_skills_dir_monkeypatch_still_wins.How to Test
agent/skill_commands.pyand runpytest tests/tools/test_skills_tool_profile_scope.py -k scan_skill_commands.Two of the three fail, resolving to the import-time home:
..._honors_an_explicit_skills_dir_monkeypatch) passes either way bydesign — it guards the escape hatch rather than reproducing the bug.
pytest tests/tools/test_skills_tool_profile_scope.py -q→ 6 passed.patches
SKILLS_DIRor exercises profile isolation:test_cross_profile_guard.py, 1 intest_skills_config.py) — I diffed the twofailure sets and they are identical, so none are attributable to this change.
Checklist
Code
fix(scope):,feat(scope):, etc.)pytest tests/ -qand all tests pass — not claimed. The full suite is not green on this machine independent of this change. I ran the 12 skill-touching suites listed above and diffed the failure sets before and after: identical, +3 passing (the new tests).Documentation & Housekeeping
docs/, docstrings) — or N/A (the reasoning lives in a code comment beside the change; a stale comment was corrected)cli-config.yaml.exampleif I added/changed config keys — or N/A (N/A — no config keys)CONTRIBUTING.mdorAGENTS.mdif I changed architecture or workflows — or N/A (N/A — completes an existing migration)_skills_dir()returns apathlib.Pathexactly asSKILLS_DIRdid; no new path handling)Screenshots / Logs
Probe showing why the constant cannot follow a scoped request, and that the live
resolver can — the process imports under one home, then binds an override to another: