From 66223e5bbfa87bc99eda51d0d34cd09cce668a95 Mon Sep 17 00:00:00 2001 From: LeonSGP43 <154585401+LeonSGP43@users.noreply.github.com> Date: Wed, 22 Apr 2026 01:48:55 +0800 Subject: [PATCH] fix(skills): support category-qualified local skill names --- tests/test_plugin_skills.py | 17 +++++++++++++++++ tools/skills_tool.py | 18 ++++++++++++++++-- 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/tests/test_plugin_skills.py b/tests/test_plugin_skills.py index 2784ba7828..9764da92b6 100644 --- a/tests/test_plugin_skills.py +++ b/tests/test_plugin_skills.py @@ -241,6 +241,23 @@ class TestSkillViewQualifiedName: assert result["success"] is False assert "not found" in result["error"].lower() + def test_category_qualified_local_skill_falls_through(self, tmp_path, monkeypatch): + from tools.skills_tool import skill_view + + local_skills = tmp_path / "local-skills" + skill_dir = local_skills / "productivity" / "ticktick" + skill_dir.mkdir(parents=True) + (skill_dir / "SKILL.md").write_text( + "---\nname: ticktick\ndescription: local categorized\n---\nTickTick body.\n" + ) + monkeypatch.setattr("tools.skills_tool.SKILLS_DIR", local_skills) + + result = json.loads(skill_view("productivity:ticktick")) + + assert result["success"] is True + assert result["name"] == "ticktick" + assert "TickTick body." in result["content"] + def test_stale_entry_self_heals(self, tmp_path): from tools.skills_tool import skill_view diff --git a/tools/skills_tool.py b/tools/skills_tool.py index 6ff54230d5..9990380aa4 100644 --- a/tools/skills_tool.py +++ b/tools/skills_tool.py @@ -834,6 +834,7 @@ def skill_view(name: str, file_path: str = None, task_id: str = None) -> str: JSON string with skill content or error message """ try: + local_category_name: str | None = None # ── Qualified name dispatch (plugin skills) ────────────────── # Names containing ':' are routed to the plugin skill registry. # Bare names fall through to the existing flat-tree scan below. @@ -888,8 +889,12 @@ def skill_view(name: str, file_path: str = None, task_id: str = None) -> str: }, ensure_ascii=False, ) - # Plugin itself not found — fall through to flat-tree scan - # which will return a normal "not found" with suggestions. + # Plugin itself not found — fall through to flat-tree scan. + # Categorized local skills also use `category:skill` in config and + # gateway prompts, so preserve that form and translate it to the + # on-disk `category/skill` path during the local scan below. + if bare: + local_category_name = f"{namespace}/{bare}" from agent.skill_utils import get_external_skills_dirs @@ -922,6 +927,15 @@ def skill_view(name: str, file_path: str = None, task_id: str = None) -> str: elif direct_path.with_suffix(".md").exists(): skill_md = direct_path.with_suffix(".md") break + if local_category_name: + categorized_path = search_dir / local_category_name + if categorized_path.is_dir() and (categorized_path / "SKILL.md").exists(): + skill_dir = categorized_path + skill_md = categorized_path / "SKILL.md" + break + elif categorized_path.with_suffix(".md").exists(): + skill_md = categorized_path.with_suffix(".md") + break # Search by directory name across all dirs if not skill_md: