fix(skills): ignore support docs in skill discovery
Support files under references/, templates/, assets/, and scripts/ are progressive-disclosure data loaded through skill_view(..., file_path=...). They should not be treated as standalone skills during discovery or collision checks. This prevents archived skill packages or support markdown files inside a real skill from shadowing active skills with the same name while still allowing top-level categories named scripts/templates/assets/references. Tests cover: - pruning nested SKILL.md files inside skill support directories - preserving support-named top-level categories - avoiding skill_view collisions from support markdown - keeping archived package SKILL.md files accessible only through file_path
This commit is contained in:
committed by
Teknium
parent
7493de7fc3
commit
9137b86a52
@@ -6,6 +6,8 @@ from agent.skill_utils import (
|
||||
extract_skill_conditions,
|
||||
get_disabled_skill_names,
|
||||
get_external_skills_dirs,
|
||||
is_excluded_skill_path,
|
||||
is_skill_support_path,
|
||||
iter_skill_index_files,
|
||||
resolve_skill_config_values,
|
||||
skill_matches_platform,
|
||||
@@ -166,6 +168,51 @@ def test_skill_config_raw_cache_invalidates_on_config_edit(tmp_path, monkeypatch
|
||||
os.utime(config_path, None)
|
||||
|
||||
assert get_disabled_skill_names() == {"new-skill"}
|
||||
def test_iter_skill_index_files_prunes_skill_support_dirs(tmp_path):
|
||||
"""Archived package SKILL.md files under support dirs are not active skills."""
|
||||
real = tmp_path / "umbrella"
|
||||
real.mkdir()
|
||||
(real / "SKILL.md").write_text("---\nname: umbrella\n---\n", encoding="utf-8")
|
||||
|
||||
package = real / "references" / "old-skill-package"
|
||||
package.mkdir(parents=True)
|
||||
(package / "SKILL.md").write_text("---\nname: old-skill\n---\n", encoding="utf-8")
|
||||
(package / "DESCRIPTION.md").write_text(
|
||||
"---\ndescription: archived package\n---\n", encoding="utf-8"
|
||||
)
|
||||
|
||||
script_package = real / "scripts" / "helper-skill"
|
||||
script_package.mkdir(parents=True)
|
||||
(script_package / "SKILL.md").write_text("---\nname: helper\n---\n", encoding="utf-8")
|
||||
|
||||
found = list(iter_skill_index_files(tmp_path, "SKILL.md"))
|
||||
desc_found = list(iter_skill_index_files(tmp_path, "DESCRIPTION.md"))
|
||||
|
||||
assert found == [real / "SKILL.md"]
|
||||
assert desc_found == []
|
||||
assert is_skill_support_path(package / "SKILL.md") is True
|
||||
assert is_excluded_skill_path(package / "SKILL.md") is True
|
||||
|
||||
|
||||
def test_iter_skill_index_files_keeps_support_named_categories(tmp_path):
|
||||
"""A category named scripts/templates/assets/references is still valid."""
|
||||
scripts_skill = tmp_path / "scripts" / "bash-helper"
|
||||
scripts_skill.mkdir(parents=True)
|
||||
(scripts_skill / "SKILL.md").write_text(
|
||||
"---\nname: bash-helper\n---\n", encoding="utf-8"
|
||||
)
|
||||
|
||||
templates_skill = tmp_path / "templates" / "deck-template"
|
||||
templates_skill.mkdir(parents=True)
|
||||
(templates_skill / "SKILL.md").write_text(
|
||||
"---\nname: deck-template\n---\n", encoding="utf-8"
|
||||
)
|
||||
|
||||
found = list(iter_skill_index_files(tmp_path, "SKILL.md"))
|
||||
|
||||
assert found == [scripts_skill / "SKILL.md", templates_skill / "SKILL.md"]
|
||||
assert is_skill_support_path(scripts_skill / "SKILL.md") is False
|
||||
assert is_excluded_skill_path(scripts_skill / "SKILL.md") is False
|
||||
|
||||
|
||||
# ── skill_matches_platform on Termux ──────────────────────────────────────
|
||||
|
||||
@@ -1225,6 +1225,89 @@ class TestSkillViewCollisionDetection:
|
||||
assert result["success"] is True
|
||||
assert "LOCAL VERSION" in result["content"]
|
||||
|
||||
def test_support_markdown_does_not_collide_with_real_skill(self, tmp_path):
|
||||
"""Supporting reference docs named <skill>.md are not skills.
|
||||
|
||||
A real-world regression had creative/sketch/SKILL.md become
|
||||
unloadable because another skill carried
|
||||
references/styles/sketch.md. Support files are loaded via
|
||||
skill_view(skill, file_path=...), not as bare skill names.
|
||||
"""
|
||||
local_dir = tmp_path / "local"
|
||||
external_dir = tmp_path / "external"
|
||||
local_dir.mkdir()
|
||||
external_dir.mkdir()
|
||||
|
||||
_make_skill(local_dir, "article-illustrator", category="creative")
|
||||
support_file = (
|
||||
local_dir
|
||||
/ "creative"
|
||||
/ "article-illustrator"
|
||||
/ "references"
|
||||
/ "styles"
|
||||
/ "sketch.md"
|
||||
)
|
||||
support_file.parent.mkdir(parents=True, exist_ok=True)
|
||||
support_file.write_text("# Sketch style support doc\n")
|
||||
_make_skill(local_dir, "sketch", category="creative", body="REAL SKETCH SKILL")
|
||||
|
||||
p1, p2 = self._patch_dirs(local_dir, [external_dir])
|
||||
with p1, p2:
|
||||
raw = skill_view("sketch")
|
||||
|
||||
result = json.loads(raw)
|
||||
assert result["success"] is True
|
||||
assert result["path"] == "creative/sketch/SKILL.md"
|
||||
assert "REAL SKETCH SKILL" in result["content"]
|
||||
|
||||
def test_reference_package_skill_md_is_not_active_skill(self, tmp_path):
|
||||
"""Curator-preserved package SKILL.md files under references stay data.
|
||||
|
||||
Umbrella consolidations may preserve an old skill as
|
||||
references/old-skill-package/SKILL.md. That package must not appear in
|
||||
skills_list/system prompts and must not resolve as skill_view("old-skill").
|
||||
The package can still be opened explicitly through the umbrella's
|
||||
file_path progressive-disclosure channel.
|
||||
"""
|
||||
local_dir = tmp_path / "local"
|
||||
external_dir = tmp_path / "external"
|
||||
local_dir.mkdir()
|
||||
external_dir.mkdir()
|
||||
|
||||
_make_skill(local_dir, "umbrella", category="creative", body="UMBRELLA")
|
||||
package = (
|
||||
local_dir
|
||||
/ "creative"
|
||||
/ "umbrella"
|
||||
/ "references"
|
||||
/ "old-skill-package"
|
||||
)
|
||||
package.mkdir(parents=True, exist_ok=True)
|
||||
(package / "SKILL.md").write_text(
|
||||
"---\nname: old-skill\ndescription: Preserved old skill.\n---\n\nOLD BODY\n"
|
||||
)
|
||||
|
||||
p1, p2 = self._patch_dirs(local_dir, [external_dir])
|
||||
with p1, p2:
|
||||
names = {skill["name"] for skill in _find_all_skills()}
|
||||
old_raw = skill_view("old-skill")
|
||||
direct_package_raw = skill_view("creative/umbrella/references/old-skill-package")
|
||||
package_raw = skill_view(
|
||||
"umbrella", file_path="references/old-skill-package/SKILL.md"
|
||||
)
|
||||
|
||||
assert "umbrella" in names
|
||||
assert "old-skill" not in names
|
||||
old_result = json.loads(old_raw)
|
||||
assert old_result["success"] is False
|
||||
assert "not found" in old_result["error"]
|
||||
direct_package_result = json.loads(direct_package_raw)
|
||||
assert direct_package_result["success"] is False
|
||||
assert "not found" in direct_package_result["error"]
|
||||
package_result = json.loads(package_raw)
|
||||
assert package_result["success"] is True
|
||||
assert "OLD BODY" in package_result["content"]
|
||||
|
||||
def test_external_skill_resolves_when_no_collision(self, tmp_path):
|
||||
"""External-only skills still resolve normally when there's no
|
||||
local skill of the same name."""
|
||||
|
||||
Reference in New Issue
Block a user