diff --git a/semif_agent/skills.py b/semif_agent/skills.py index 4e2a0e8..68d1752 100644 --- a/semif_agent/skills.py +++ b/semif_agent/skills.py @@ -430,8 +430,8 @@ def parse_skill_draft(raw: str) -> SkillDraft: if not title or not description: raise ValueError(f"skill draft missing title/description: {raw!r}") name = re.sub(r"\s+", "_", title.lower()) - if not name.replace("_", "").isalnum(): - raise ValueError(f"skill title must be snake_case alnum: {title!r}") + if not re.fullmatch(r"[a-z0-9_]+(?:\.[a-z0-9_]+)*", name): + raise ValueError(f"skill title must be snake_case alnum (dots allowed): {title!r}") return SkillDraft(name=name, description=description) diff --git a/tests/test_skills.py b/tests/test_skills.py index a8d6ab4..ef92742 100644 --- a/tests/test_skills.py +++ b/tests/test_skills.py @@ -140,6 +140,13 @@ def test_parse_skill_draft_normalizes_title(): assert draft.name == "live_tracking" +def test_parse_skill_draft_allows_dotted_name(): + draft = parse_skill_draft( + '{"title": "tracking.status_lookup", "description": "Look up a package status."}' + ) + assert draft.name == "tracking.status_lookup" + + def test_parse_skill_draft_missing_fields_raises(): with pytest.raises(ValueError): parse_skill_draft('{"title": "only_title"}')