{"record":{"id":"178cde08cc4e0f6c","repo":"github/spec-kit","slug":"template-template-name-has-composing-layers-bu","errorCode":null,"errorMessage":"Template '{template_name}' has composing layers but no replace base","messagePattern":"Template '(.+?)' has composing layers but no replace base","errorType":"exception","errorClass":"TemplateResolutionError","httpStatus":null,"severity":"error","filePath":"scripts/python/common.py","lineNumber":513,"sourceCode":"                return compose_from_base()\n\n    extensions_dir = repo_root / \".specify\" / \"extensions\"\n    for extension_id in _sorted_extension_ids(extensions_dir):\n        extension_dir = extensions_dir / extension_id\n        candidate = _conventional_template(extension_dir, template_name)\n        if candidate is not None:\n            layers.append((candidate, \"replace\"))\n            return compose_from_base()\n\n    core = repo_root / \".specify\" / \"templates\" / f\"{template_name}.md\"\n    if core.is_file():\n        layers.append((core, \"replace\"))\n        return compose_from_base()\n\n    if not layers:\n        return None\n\n    raise TemplateResolutionError(\n        f\"Template '{template_name}' has composing layers but no replace base\"\n    )\n\n\ndef get_invoke_separator(repo_root: Path) -> str:\n    integration_json = repo_root / \".specify\" / \"integration.json\"\n    if not integration_json.is_file():\n        return \".\"\n    # Split the parse out of the lookup and guard the top-level shape, matching\n    # read_feature_json_feature_directory above and the bash/PowerShell twins,\n    # which both fall back to \".\" for any unusable integration.json:\n    #   * a non-mapping top level ([], \"forge\", 42, null) is valid JSON, so\n    #     json.JSONDecodeError never fires and state.get(...) raised\n    #     AttributeError;\n    #   * a non-UTF-8 file raises UnicodeDecodeError, which is a ValueError --\n    #     not an OSError -- so it escaped the except tuple. Realistic on\n    #     Windows, where PowerShell 5.1's Out-File/`>` default to UTF-16.\n    try:","sourceCodeStart":495,"sourceCodeEnd":531,"githubUrl":"https://github.com/github/spec-kit/blob/bf88c9f9a82fa370c7a7257aa2b3cf10b457b65c/scripts/python/common.py#L495-L531","documentation":"Layer composition requires exactly one base layer with strategy \"replace\" (an explicit replace candidate or the core template at .specify/templates/<name>.md) onto which prepend/append/wrap layers are applied. If only composing layers were found — no override file, no replace preset layer, and no core template — the function raises TemplateResolutionError instead of returning partial content.","triggerScenarios":"A user creates .specify/templates/presets/<p>/<name>.md with strategy prepend/append/wrap but deletes or never creates the core .specify/templates/<name>.md and provides no replace layer; or the core template filename casing mismatches the requested template_name.","commonSituations":"Removing a stock template thinking presets alone are enough; renaming a template file but not the preset references; a typo in template_name so the core file lookup misses while preset layers with looser matching still hit.","solutions":["Restore or create the base template at .specify/templates/<template_name>.md (exact name from the error message).","Alternatively add a layer with strategy: replace in the preset so the stack has a base.","Check filename casing/spelling of both the requested template name and the files on disk."],"exampleFix":"# before — only .specify/templates/presets/team/clarify.md (strategy: prepend), no base\n# after — also restore the base file\n# git checkout HEAD -- .specify/templates/clarify.md","handlingStrategy":"validation","validationCode":"from pathlib import Path\n\ndef has_replace_base(repo_root: Path, template_name: str, replace_layers: list[Path]) -> bool:\n    if replace_layers:\n        return True\n    return (repo_root / \".specify\" / \"templates\" / f\"{template_name}.md\").is_file()\n\n# guard before enabling prepend/append/wrap presets\nassert has_replace_base(repo_root, name, replace_layers), \"no replace base for layers\"","typeGuard":null,"tryCatchPattern":"try:\n    content = resolve_template_content(name, repo_root)\nexcept TemplateResolutionError as exc:\n    if \"no replace base\" in str(exc):\n        raise SystemExit(f\"Restore the base template for {exc}\") from exc\n    raise","preventionTips":["Never delete core .specify/templates/*.md files while keeping composing presets.","When adding presets, verify a base (core template or a replace layer) exists.","Treat template_name casing as significant — match filenames exactly."],"tags":["templates","preset","composition","missing-file"],"backgroundTag":null,"analyzedSha":"bf88c9f9a82fa370c7a7257aa2b3cf10b457b65c","analyzedAt":"2026-08-14T19:43:37.150Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}