Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions tests/tools/test_skills_tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -1062,6 +1062,42 @@ def test_support_markdown_does_not_collide_with_real_skill(self, tmp_path):
assert "REAL SKETCH SKILL" in result["content"]


def test_package_owned_markdown_does_not_collide_with_real_skill(self, tmp_path):
local_dir = tmp_path / "local"
local_dir.mkdir()
_make_skill(local_dir, "research", body="REAL RESEARCH SKILL")
_make_skill(local_dir, "example", category="character")
prompt = local_dir / "character" / "example" / "prompts" / "research.md"
prompt.parent.mkdir()
prompt.write_text("# Internal research prompt\n", encoding="utf-8")

p1, p2 = self._patch_dirs(local_dir, [])
with p1, p2:
raw = skill_view("research")
internal_raw = skill_view("character/example/prompts/research")

result = json.loads(raw)
assert result["success"] is True
assert Path(result["path"]).parts == ("research", "SKILL.md")
assert "REAL RESEARCH SKILL" in result["content"]
assert json.loads(internal_raw)["success"] is False

def test_categorized_legacy_flat_markdown_remains_loadable(self, tmp_path):
category = tmp_path / "legacy"
category.mkdir()
(category / "research.md").write_text(
"---\nname: research\ndescription: Legacy research skill.\n---\n",
encoding="utf-8",
)

p1, p2 = self._patch_dirs(tmp_path, [])
with p1, p2:
result = json.loads(skill_view("legacy/research"))

assert result["success"] is True
assert Path(result["path"]).parts == ("legacy", "research.md")


def test_two_externals_same_name_also_refuse(self, tmp_path):
"""Collision detection is symmetric — two external dirs with
same-name skills also trigger the refusal."""
Expand Down
26 changes: 20 additions & 6 deletions tools/skills_tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,18 @@ def _under_any(path: Path, dirs) -> bool:
return any(resolved.is_relative_to(d) for d in dirs)


def _is_package_owned_markdown(path: Path, search_root: Path) -> bool:
"""True when a legacy Markdown candidate belongs to an ancestor directory skill."""
try:
relative = path.relative_to(search_root)
except ValueError:
return False
return any(
(search_root.joinpath(*relative.parts[:depth]) / "SKILL.md").is_file()
for depth in range(1, len(relative.parts))
)


def _collect_skill_candidates(name, local_category_name, all_dirs):
"""ALL (skill_dir, skill_md) candidates across every dir and lookup strategy (direct path,
recursive by dir / frontmatter name, legacy flat <name>.md), deduped by resolved path.
Expand All @@ -351,26 +363,28 @@ def _record(sd: Optional[Path], smd: Path) -> None:
seen_md.add(key)
candidates.append((sd, smd))

def _record_direct(direct_path: Path) -> None: # "mlops/axolotl" / "axolotl" or its flat .md sibling
def _record_direct(direct_path: Path, search_root: Path) -> None: # "mlops/axolotl" / "axolotl" or its flat .md sibling
flat = direct_path.with_suffix(".md")
if not _is_skill_support_path(direct_path) and direct_path.is_dir() and (direct_path / "SKILL.md").exists():
_record(direct_path, direct_path / "SKILL.md")
elif flat.exists() and not _is_skill_support_path(flat):
elif (flat.exists() and not _is_skill_support_path(flat)
and not _is_package_owned_markdown(flat, search_root)):
_record(None, flat)

for search_dir in all_dirs:
for direct in filter(None, (name, local_category_name)): # "p:x" with no plugin p → "p/x"
_record_direct(search_dir / direct)
_record_direct(search_dir / direct, search_dir)
# Recursive by directory name plus frontmatter `name:` — skills_list()
# exposes the frontmatter name, so skill_view(name) must accept it too.
for found_skill_md in iter_skill_index_files(search_dir, "SKILL.md"):
if (found_skill_md.parent.name == name
or _safe_frontmatter(found_skill_md).get("name") == name):
_record(found_skill_md.parent, found_skill_md)
# Legacy flat <name>.md anywhere under the dir; support docs are excluded
# (they load via file_path and must not shadow real skills sharing the basename).
# Legacy flat <name>.md anywhere under the dir. Markdown owned by an ancestor
# directory skill loads through file_path and must not shadow a real skill.
for found_md in search_dir.rglob(f"{name}.md"):
if found_md.name != "SKILL.md" and not _is_skill_support_path(found_md):
if (found_md.name != "SKILL.md" and not _is_skill_support_path(found_md)
and not _is_package_owned_markdown(found_md, search_dir)):
_record(None, found_md)
return candidates

Expand Down