From 729187e66944ab7645a29560a27ed515baea87d9 Mon Sep 17 00:00:00 2001 From: MarceloCorreiaData Date: Tue, 17 Mar 2026 22:12:44 -0400 Subject: [PATCH 1/4] fix(skills): auto-convert underscores to hyphens in Frontmatter name validator Python cannot import modules whose names contain hyphens, but the ADK skill spec requires kebab-case names in SKILL.md frontmatter. This normalizes underscores to hyphens before validation, so both snake_case and kebab-case are accepted. --- src/google/adk/skills/models.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/google/adk/skills/models.py b/src/google/adk/skills/models.py index cda1447bbe..5e2f26c550 100644 --- a/src/google/adk/skills/models.py +++ b/src/google/adk/skills/models.py @@ -76,6 +76,9 @@ def _validate_metadata(cls, v: dict[str, Any]) -> dict[str, Any]: @classmethod def _validate_name(cls, v: str) -> str: v = unicodedata.normalize("NFKC", v) + # Normalize underscores to hyphens so snake_case directory names + # work transparently (Python cannot import modules with hyphens). + v = v.replace("_", "-") if len(v) > 64: raise ValueError("name must be at most 64 characters") if not _NAME_PATTERN.match(v): From 836a8e91355e09ad3eda007696be54c5e627d094 Mon Sep 17 00:00:00 2001 From: MarceloCorreiaData Date: Tue, 17 Mar 2026 22:19:05 -0400 Subject: [PATCH 2/4] fix(skills): normalize directory names in all skill name-matching checks Allow snake_case directory names to match kebab-case frontmatter names in _load_skill_from_dir, _validate_skill_dir, _list_skills_in_dir, and _load_skill_from_gcs_dir. --- src/google/adk/skills/_utils.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/google/adk/skills/_utils.py b/src/google/adk/skills/_utils.py index 0fdb553ee3..a9f90d1ab2 100644 --- a/src/google/adk/skills/_utils.py +++ b/src/google/adk/skills/_utils.py @@ -149,8 +149,10 @@ def _load_skill_from_dir(skill_dir: Union[str, pathlib.Path]) -> models.Skill: # Use model_validate to handle aliases like allowed-tools frontmatter = models.Frontmatter.model_validate(parsed) - # Validate that skill name matches the directory name - if skill_dir.name != frontmatter.name: + # Validate that skill name matches the directory name. + # Allow underscore directories to match kebab-case frontmatter names, + # since Python cannot import modules whose names contain hyphens. + if skill_dir.name.replace("_", "-") != frontmatter.name: raise ValueError( f"Skill name '{frontmatter.name}' does not match directory" f" name '{skill_dir.name}'." @@ -222,7 +224,7 @@ def _validate_skill_dir( problems.append(f"Frontmatter validation error: {e}") return problems - if skill_dir.name != frontmatter.name: + if skill_dir.name.replace("_", "-") != frontmatter.name: problems.append( f"Skill name '{frontmatter.name}' does not match directory" f" name '{skill_dir.name}'." @@ -281,7 +283,7 @@ def _list_skills_in_dir( skill_id = skill_dir.name try: frontmatter = _read_skill_properties(skill_dir) - if skill_id != frontmatter.name: + if skill_id.replace("_", "-") != frontmatter.name: raise ValueError( f"Skill name '{frontmatter.name}' does not match directory" f" name '{skill_id}'." @@ -387,7 +389,7 @@ def _load_skill_from_gcs_dir( # Validate that skill name matches the directory name skill_name_expected = skill_id.strip("/").split("/")[-1] - if skill_name_expected != frontmatter.name: + if skill_name_expected.replace("_", "-") != frontmatter.name: raise ValueError( f"Skill name '{frontmatter.name}' does not match directory" f" name '{skill_name_expected}'." From efe2c39f65f03a5466350ba2028980b0a70886aa Mon Sep 17 00:00:00 2001 From: MarceloCorreiaData Date: Tue, 17 Mar 2026 22:29:43 -0400 Subject: [PATCH 3/4] test(skills): update name validation tests for underscore normalization --- tests/unittests/skills/test_models.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/tests/unittests/skills/test_models.py b/tests/unittests/skills/test_models.py index 5685e9d868..867412f1ee 100644 --- a/tests/unittests/skills/test_models.py +++ b/tests/unittests/skills/test_models.py @@ -99,9 +99,16 @@ def test_name_consecutive_hyphens(): models.Frontmatter(name="my--skill", description="desc") -def test_name_invalid_chars_underscore(): - with pytest.raises(ValidationError, match="lowercase kebab-case"): - models.Frontmatter(name="my_skill", description="desc") +def test_name_underscore_normalized_to_kebab(): + """Tests that underscores in name are normalized to hyphens.""" + fm = models.Frontmatter(name="my_skill", description="desc") + assert fm.name == "my-skill" + + +def test_name_mixed_underscore_hyphen(): + """Tests mixed underscores and hyphens are normalized.""" + fm = models.Frontmatter(name="my_cool-skill", description="desc") + assert fm.name == "my-cool-skill" def test_name_invalid_chars_ampersand(): From 9b8fae1a77c63a94dad985132ebbcd033873eb9e Mon Sep 17 00:00:00 2001 From: MarceloCorreiaData Date: Tue, 17 Mar 2026 22:31:20 -0400 Subject: [PATCH 4/4] test(skills): add underscore dir with kebab-case name compatibility tests --- tests/unittests/skills/test__utils.py | 54 +++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/tests/unittests/skills/test__utils.py b/tests/unittests/skills/test__utils.py index ae5df00c1c..6d0f9dbd39 100644 --- a/tests/unittests/skills/test__utils.py +++ b/tests/unittests/skills/test__utils.py @@ -340,3 +340,57 @@ def test_list_skills_in_dir_missing_base_path(tmp_path): skills = list_skills_in_dir(tmp_path / "nonexistent") assert skills == {} + + +# --- Underscore directory / kebab-case name compatibility tests --- + + +def test_load_skill_underscore_dir_kebab_name(tmp_path): + """Tests loading a skill from underscore dir with kebab-case name.""" + skill_dir = tmp_path / "my_skill" + skill_dir.mkdir() + + skill_md = """--- +name: my-skill +description: A skill +--- +Body +""" + (skill_dir / "SKILL.md").write_text(skill_md) + + skill = _load_skill_from_dir(skill_dir) + assert skill.name == "my-skill" + + +def test_validate_skill_dir_underscore_dir_kebab_name(tmp_path): + """Tests validate_skill_dir accepts underscore dir with kebab name.""" + skill_dir = tmp_path / "my_skill" + skill_dir.mkdir() + + skill_md = """--- +name: my-skill +description: A skill +--- +Body +""" + (skill_dir / "SKILL.md").write_text(skill_md) + + problems = _validate_skill_dir(skill_dir) + assert problems == [] + + +def test_list_skills_underscore_dir(tmp_path): + """Tests listing skills with underscore directory names.""" + skills_dir = tmp_path / "skills" + skills_dir.mkdir() + + skill_dir = skills_dir / "my_skill" + skill_dir.mkdir() + (skill_dir / "SKILL.md").write_text( + "---\nname: my-skill\ndescription: desc\n---\nbody" + ) + + skills = list_skills_in_dir(skills_dir) + assert len(skills) == 1 + assert "my_skill" in skills + assert skills["my_skill"].name == "my-skill"