github/spec-kit · error · ValidationError
Invalid {singular} entry '{name}': 'strategy' is not authora
Error message
Invalid {singular} entry '{name}': 'strategy' is not authorable for extension-provided artifacts, which always use 'replace' semantics What it means
A provides.templates/scripts entry contains a 'strategy' key. 'strategy' (merge/replace layering semantics) is authorable only in presets; extension-provided artifacts ALWAYS use 'replace' semantics, so the key is rejected rather than silently ignored. This protects authors who copy a preset-style entry from getting a silently-dropped field — the error names the exact entry.
Source
Thrown at src/specify_cli/extensions/__init__.py:623
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"
)
if "strategy" in entry:
raise ValidationError(
f"Invalid {singular} entry '{name}': 'strategy' is not authorable for "
"extension-provided artifacts, which always use 'replace' semantics"
)
if section == "scripts" and "runtimes" in entry:
runtimes = entry["runtimes"]
if not isinstance(runtimes, list) or not all(
isinstance(r, str) for r in runtimes
):
raise ValidationError(
f"Invalid runtimes for script '{name}': expected a list of strings"
)
invalid = sorted(set(runtimes) - VALID_SCRIPT_RUNTIMES)
if invalid:
raise ValidationError(
f"Invalid runtimes {invalid} for script '{name}': "
f"must be one of {sorted(VALID_SCRIPT_RUNTIMES)}"
)View on GitHub (pinned to bf88c9f9a8)
Solutions
- Delete the 'strategy' key from the entry — extension artifacts always replace.
- If you need merge/layering behavior, express it via a preset rather than an extension-provided artifact.
- If you depended on merging, restructure so the extension template is complete on its own.
Example fix
// before
{ "name": "plan", "file": "templates/plan.md", "strategy": "merge" }
// after
{ "name": "plan", "file": "templates/plan.md" } Defensive patterns
Strategy: validation
Validate before calling
for section in ("templates", "scripts"):
for e in manifest.get("provides", {}).get(section, []):
if isinstance(e, dict):
assert "strategy" not in e, f"'strategy' is not allowed on extension {section}: {e.get('name')}" Type guard
def has_no_strategy_key(entry: dict) -> bool:
return "strategy" not in entry Try / catch
try:
ExtensionManifest.load(path)
except ValidationError as e:
if "'strategy' is not authorable" in str(e):
# delete the strategy key; extension artifacts always replace
... Prevention
- strategy belongs to presets only, never to extension artifacts.
- When copying a preset entry into an extension, strip 'strategy'.
- Assume replace semantics when designing extension templates.
When it happens
Trigger: {"name": "plan", "file": "...", "strategy": "merge"} in provides.templates or provides.scripts. Raised by the 'strategy' in entry check in the artifact validator (contrast with presets/__init__.py where strategy is legal).
Common situations: Copy-pasting an entry from a preset file (where strategy is valid) into an extension manifest; assuming merge semantics exist for extension templates.
Related errors
- Manifest must be a YAML mapping, got {type(data).__name__}:
- Missing required field: {field}
- Invalid extension: expected a mapping, got {type(ext).__name
- Missing extension.{field}
- Invalid extension.{field}: expected a string, got {type(ext[
AI-assisted analysis of github/spec-kit@bf88c9f9a8 (2026-08-14).
Data as JSON: /api/errors/cd22ebc420dbe484.
Report an issue: GitHub.