diff --git a/agent/prompt_builder.py b/agent/prompt_builder.py index 4ce84473f5..8dc3124ba9 100644 --- a/agent/prompt_builder.py +++ b/agent/prompt_builder.py @@ -330,28 +330,34 @@ def build_skills_system_prompt( # Each entry: (skill_name, description) # Supports sub-categories: skills/mlops/training/axolotl/SKILL.md # -> category "mlops/training", skill "axolotl" + # Load disabled skill names once for the entire scan + try: + from tools.skills_tool import _get_disabled_skill_names + disabled = _get_disabled_skill_names() + except Exception: + disabled = set() + skills_by_category: dict[str, list[tuple[str, str]]] = {} for skill_file in skills_dir.rglob("SKILL.md"): - is_compatible, _, desc = _parse_skill_file(skill_file) + is_compatible, frontmatter, desc = _parse_skill_file(skill_file) if not is_compatible: continue - # Skip skills whose conditional activation rules exclude them - conditions = _read_skill_conditions(skill_file) - if not _skill_should_show(conditions, available_tools, available_toolsets): - continue rel_path = skill_file.relative_to(skills_dir) parts = rel_path.parts if len(parts) >= 2: - # Category is everything between skills_dir and the skill folder - # e.g. parts = ("mlops", "training", "axolotl", "SKILL.md") - # → category = "mlops/training", skill_name = "axolotl" - # e.g. parts = ("github", "github-auth", "SKILL.md") - # → category = "github", skill_name = "github-auth" skill_name = parts[-2] category = "/".join(parts[:-2]) if len(parts) > 2 else parts[0] else: category = "general" skill_name = skill_file.parent.name + # Respect user's disabled skills config + fm_name = frontmatter.get("name", skill_name) + if fm_name in disabled or skill_name in disabled: + continue + # Skip skills whose conditional activation rules exclude them + conditions = _read_skill_conditions(skill_file) + if not _skill_should_show(conditions, available_tools, available_toolsets): + continue skills_by_category.setdefault(category, []).append((skill_name, desc)) if not skills_by_category: diff --git a/agent/skill_commands.py b/agent/skill_commands.py index 67315ee8df..b266ad251c 100644 --- a/agent/skill_commands.py +++ b/agent/skill_commands.py @@ -157,9 +157,10 @@ def scan_skill_commands() -> Dict[str, Dict[str, Any]]: global _skill_commands _skill_commands = {} try: - from tools.skills_tool import SKILLS_DIR, _parse_frontmatter, skill_matches_platform + from tools.skills_tool import SKILLS_DIR, _parse_frontmatter, skill_matches_platform, _get_disabled_skill_names if not SKILLS_DIR.exists(): return _skill_commands + disabled = _get_disabled_skill_names() for skill_md in SKILLS_DIR.rglob("SKILL.md"): if any(part in ('.git', '.github', '.hub') for part in skill_md.parts): continue @@ -170,6 +171,9 @@ def scan_skill_commands() -> Dict[str, Dict[str, Any]]: if not skill_matches_platform(frontmatter): continue name = frontmatter.get('name', skill_md.parent.name) + # Respect user's disabled skills config + if name in disabled: + continue description = frontmatter.get('description', '') if not description: for line in body.strip().split('\n'): diff --git a/tests/agent/test_prompt_builder.py b/tests/agent/test_prompt_builder.py index 1de37efbe5..07c8da1891 100644 --- a/tests/agent/test_prompt_builder.py +++ b/tests/agent/test_prompt_builder.py @@ -309,6 +309,35 @@ class TestBuildSkillsSystemPrompt: assert "imessage" in result assert "Send iMessages" in result + def test_excludes_disabled_skills(self, monkeypatch, tmp_path): + """Skills in the user's disabled list should not appear in the system prompt.""" + monkeypatch.setenv("HERMES_HOME", str(tmp_path)) + skills_dir = tmp_path / "skills" / "tools" + skills_dir.mkdir(parents=True) + + enabled_skill = skills_dir / "web-search" + enabled_skill.mkdir() + (enabled_skill / "SKILL.md").write_text( + "---\nname: web-search\ndescription: Search the web\n---\n" + ) + + disabled_skill = skills_dir / "old-tool" + disabled_skill.mkdir() + (disabled_skill / "SKILL.md").write_text( + "---\nname: old-tool\ndescription: Deprecated tool\n---\n" + ) + + from unittest.mock import patch + + with patch( + "tools.skills_tool._get_disabled_skill_names", + return_value={"old-tool"}, + ): + result = build_skills_system_prompt() + + assert "web-search" in result + assert "old-tool" not in result + def test_includes_setup_needed_skills(self, monkeypatch, tmp_path): monkeypatch.setenv("HERMES_HOME", str(tmp_path)) monkeypatch.delenv("MISSING_API_KEY_XYZ", raising=False) diff --git a/tests/agent/test_skill_commands.py b/tests/agent/test_skill_commands.py index c024461380..f6a114db67 100644 --- a/tests/agent/test_skill_commands.py +++ b/tests/agent/test_skill_commands.py @@ -85,6 +85,21 @@ class TestScanSkillCommands: result = scan_skill_commands() assert "/generic-tool" in result + def test_excludes_disabled_skills(self, tmp_path): + """Disabled skills should not register slash commands.""" + with ( + patch("tools.skills_tool.SKILLS_DIR", tmp_path), + patch( + "tools.skills_tool._get_disabled_skill_names", + return_value={"disabled-skill"}, + ), + ): + _make_skill(tmp_path, "enabled-skill") + _make_skill(tmp_path, "disabled-skill") + result = scan_skill_commands() + assert "/enabled-skill" in result + assert "/disabled-skill" not in result + class TestBuildPreloadedSkillsPrompt: def test_builds_prompt_for_multiple_named_skills(self, tmp_path):