github/spec-kit · error · ValidationError
Invalid requires.speckit_version: expected a non-empty strin
Error message
Invalid requires.speckit_version: expected a non-empty string, got {type(requires['speckit_version']).__name__} What it means
requires.speckit_version must be a non-empty string. The source comment explains why the type is enforced strictly: check_compatibility() passes the value to SpecifierSet(), guarded only by `except InvalidSpecifier` — a non-string (float from unquoted YAML like `speckit_version: 1.0`, bool, None) raises TypeError, and a list/dict is accepted as iterable but blows up later with AttributeError inside .contains(). Both bypass the CLI's Compatibility Error handler, so validation rejects them up front with a named field.
Source
Thrown at src/specify_cli/extensions/__init__.py:366
)
if "speckit_version" not in requires:
raise ValidationError("Missing requires.speckit_version")
# Presence alone is not enough: check_compatibility() feeds this value to
# ``SpecifierSet(required)``, guarded only by ``except InvalidSpecifier``,
# which a non-string escapes two different ways. A float/int/bool/None
# raises TypeError from the constructor, while a list or dict is an
# *iterable*, so SpecifierSet accepts it and the failure surfaces much
# later as ``AttributeError: 'str' object has no attribute 'filter'`` from
# inside .contains(). Neither is a CompatibilityError, so both bypass the
# CLI's "Compatibility Error" handler and exit 1 with a raw traceback
# naming no field. An unquoted ``speckit_version: 1.0`` is an easy YAML
# slip. Mirrors the sibling IntegrationDescriptor, which already requires
# a non-empty string here.
if (
not isinstance(requires["speckit_version"], str)
or not requires["speckit_version"].strip()
):
raise ValidationError(
"Invalid requires.speckit_version: expected a non-empty string, "
f"got {type(requires['speckit_version']).__name__}"
)
# Validate provides section
provides = self.data["provides"]
if not isinstance(provides, dict):
raise ValidationError(
f"Invalid provides: expected a mapping, got {type(provides).__name__}"
)
commands = provides.get("commands", [])
templates = provides.get("templates", [])
scripts = provides.get("scripts", [])
hooks = self.data.get("hooks")
events = self.data.get("events")
if "commands" in provides and not isinstance(commands, list):
raise ValidationError("Invalid provides.commands: expected a list")View on GitHub (pinned to bf88c9f9a8)
Solutions
- Quote the value and include a specifier: `speckit_version: ">=1.0.0"`.
- Never write a bare number — YAML turns it into float/int, which this check rejects by design.
- Verify with a YAML linter that the field parses as str.
Example fix
# before (parses as float 1.0) requires:\n speckit_version: 1.0 # after requires:\n speckit_version: ">=1.0.0"
Defensive patterns
Strategy: validation
Validate before calling
def speckit_version_ok(data: dict) -> bool:
v = data.get("requires", {}).get("speckit_version")
return isinstance(v, str) and bool(v.strip()) Type guard
def is_specifier_str(v: object) -> bool:
return isinstance(v, str) and bool(v.strip()) Prevention
- Always quote: speckit_version: ">=1.0.0" — bare numbers become YAML floats that are rejected here to protect SpecifierSet.
- Include a comparison operator (>=, ==, <) so YAML cannot coerce the value to a number.
When it happens
Trigger: An unquoted `speckit_version: 1.0` in YAML parses to the float 1.0; `speckit_version: true` to a bool; `speckit_version: []` to a list. The isinstance(str)/strip check fails and names the offending type.
Common situations: Classic YAML slip: writing a version constraint without quotes and without a comparison operator, so YAML coerces it to a number. Also generated manifests that leave the field null.
Related errors
- Command missing 'name' or 'file'
- Invalid extension ID '{ext['id']}': must be lowercase alphan
- Invalid version: {ext['version']}
- Invalid extension.category: must be a non-empty string
- Invalid extension.effect '{ext.get('effect')}': must be one
AI-assisted analysis of github/spec-kit@bf88c9f9a8 (2026-08-14).
Data as JSON: /api/errors/ce6c446f53437ed3.
Report an issue: GitHub.