fix: restore alias compatibility for community extensions (#2110) (#2125)

Relax alias validation in _collect_manifest_command_names() to only
enforce the 3-part speckit.{ext}.{cmd} pattern on primary command
names. Aliases retain type and duplicate checking but are otherwise
free-form, restoring pre-#1994 behavior.

This unblocks community extensions (e.g. spec-kit-verify) that use
2-part aliases like 'speckit.verify'.

Fixes #2110
This commit is contained in:
Manfred Riem
2026-04-08 12:03:29 -05:00
committed by GitHub
parent 4d58ee945c
commit 4deb90f4f5
2 changed files with 25 additions and 21 deletions

View File

@@ -523,10 +523,11 @@ class ExtensionManager:
"""Collect command and alias names declared by a manifest. """Collect command and alias names declared by a manifest.
Performs install-time validation for extension-specific constraints: Performs install-time validation for extension-specific constraints:
- commands and aliases must use the canonical `speckit.{extension}.{command}` shape - primary commands must use the canonical `speckit.{extension}.{command}` shape
- commands and aliases must use this extension's namespace - primary commands must use this extension's namespace
- command namespaces must not shadow core commands - command namespaces must not shadow core commands
- duplicate command/alias names inside one manifest are rejected - duplicate command/alias names inside one manifest are rejected
- aliases are validated for type and uniqueness only (no pattern enforcement)
Args: Args:
manifest: Parsed extension manifest manifest: Parsed extension manifest
@@ -563,6 +564,9 @@ class ExtensionManager:
f"{kind.capitalize()} for command '{primary_name}' must be a string" f"{kind.capitalize()} for command '{primary_name}' must be a string"
) )
# Enforce canonical pattern only for primary command names;
# aliases are free-form to preserve community extension compat.
if kind == "command":
match = EXTENSION_COMMAND_NAME_PATTERN.match(name) match = EXTENSION_COMMAND_NAME_PATTERN.match(name)
if match is None: if match is None:
raise ValidationError( raise ValidationError(

View File

@@ -686,8 +686,8 @@ class TestExtensionManager:
with pytest.raises(ValidationError, match="conflicts with core command namespace"): with pytest.raises(ValidationError, match="conflicts with core command namespace"):
manager.install_from_directory(ext_dir, "0.1.0", register_commands=False) manager.install_from_directory(ext_dir, "0.1.0", register_commands=False)
def test_install_rejects_alias_without_extension_namespace(self, temp_dir, project_dir): def test_install_accepts_short_alias(self, temp_dir, project_dir):
"""Install should reject legacy short aliases that can shadow core commands.""" """Install should accept legacy short aliases for community extension compat."""
import yaml import yaml
ext_dir = temp_dir / "alias-shortcut" ext_dir = temp_dir / "alias-shortcut"
@@ -718,7 +718,7 @@ class TestExtensionManager:
(ext_dir / "commands" / "cmd.md").write_text("---\ndescription: Test\n---\n\nBody") (ext_dir / "commands" / "cmd.md").write_text("---\ndescription: Test\n---\n\nBody")
manager = ExtensionManager(project_dir) manager = ExtensionManager(project_dir)
with pytest.raises(ValidationError, match="Invalid alias 'speckit.shortcut'"): # Should not raise — short aliases are allowed
manager.install_from_directory(ext_dir, "0.1.0", register_commands=False) manager.install_from_directory(ext_dir, "0.1.0", register_commands=False)
def test_install_rejects_namespace_squatting(self, temp_dir, project_dir): def test_install_rejects_namespace_squatting(self, temp_dir, project_dir):