github/spec-kit · error · ValidationError

Invalid {singular} name '{name}': must be lowercase alphanum

Error message

Invalid {singular} name '{name}': must be lowercase alphanumeric with hyphens only

What it means

A template/script artifact name did not match ^[a-z0-9-]+$ (VALID_EXTENSION_ARTIFACT_NAME_PATTERN, extensions/__init__.py:67). Unlike commands, these names are not namespaced, but they must be a lowercase alphanumeric slug with hyphens — no uppercase, underscores, dots, or symbols. This mirrors the same constraint applied to extension.id.

Source

Thrown at src/specify_cli/extensions/__init__.py:601

        later duplicate would be silently unreachable while still being
        exposed by ``ExtensionManifest.templates``/``.scripts``.
        """
        seen_names: set[str] = set()
        for entry in entries:
            if not isinstance(entry, dict):
                raise ValidationError(
                    f"Each entry in 'provides.{section}' must be a mapping"
                )
            if "name" not in entry or "file" not in entry:
                raise ValidationError(f"{singular.capitalize()} missing 'name' or 'file'")

            name = entry["name"]
            if not isinstance(name, str):
                raise ValidationError(
                    f"Invalid {singular} name: expected a string, got {type(name).__name__}"
                )
            if not VALID_EXTENSION_ARTIFACT_NAME_PATTERN.match(name):
                raise ValidationError(
                    f"Invalid {singular} name '{name}': "
                    "must be lowercase alphanumeric with hyphens only"
                )
            if name in seen_names:
                raise ValidationError(
                    f"Duplicate {singular} name '{name}' in 'provides.{section}'"
                )
            seen_names.add(name)

            file_value = entry["file"]
            reason = relative_extension_path_violation(file_value)
            if reason:
                label = repr(file_value) if isinstance(file_value, str) else f"for {singular} '{name}'"
                raise ValidationError(f"Invalid {singular} 'file' {label}: {reason}")

            if "description" in entry and not isinstance(entry["description"], str):
                raise ValidationError(
                    f"Invalid {singular} description for '{name}': expected a string"

View on GitHub (pinned to bf88c9f9a8)

Solutions

  1. Rename to a lowercase hyphen slug: "plan-template" instead of "Plan_Template" or "plan.md".
  2. Strip dots and slashes; the name is an identifier, not a path (the path belongs in 'file').
  3. Keep 'file' as-is — only the name needs the slug form.

Example fix

// before
{ "name": "Plan_Template", "file": "templates/plan.md" }
// after
{ "name": "plan-template", "file": "templates/plan.md" }
Defensive patterns

Strategy: validation

Validate before calling

import re
SLUG = re.compile(r"^[a-z0-9-]+$")

for section in ("templates", "scripts"):
    for entry in manifest.get("provides", {}).get(section, []):
        if isinstance(entry, dict) and isinstance(entry.get("name"), str):
            assert SLUG.match(entry["name"]), f"non-slug name {entry['name']!r}"

Type guard

def is_artifact_slug(name: str) -> bool:
    import re
    return bool(re.match(r"^[a-z0-9-]+$", name))

Try / catch

try:
    ExtensionManifest.load(path)
except ValidationError as e:
    if "lowercase alphanumeric with hyphens" in str(e):
        # lowercase the name, replace _ and . with -, and retry
        ...

Prevention

When it happens

Trigger: "name": "Plan", "name": "plan_template", "name": "plan.md", or "name": "plan/v1". Raised by the regex check right after the string-type check in the artifact validator.

Common situations: Reusing a file basename with an extension (plan.md) as the name; PascalCase names from code constants; underscores carried over from Python identifiers.

Related errors


AI-assisted analysis of github/spec-kit@bf88c9f9a8 (2026-08-14). Data as JSON: /api/errors/0a38b5423328f2b9. Report an issue: GitHub.