{"record":{"id":"c941d0c3e89bdbb7","repo":"bmad-code-org/BMAD-METHOD","slug":"customization-tokens-require-customize-toml","errorCode":null,"errorMessage":"customization tokens require customize.toml","messagePattern":"customization tokens require customize\\.toml","errorType":"exception","errorClass":"RenderError","httpStatus":null,"severity":"error","filePath":"src/scripts/render_skill.py","lineNumber":218,"sourceCode":"    input_values: dict[str, Any] = {}\n    for content in sources.values():\n        for match in _SHORT_CONFIG_TOKEN.finditer(content):\n            token, key = match.group(0), match.group(1)\n            path, resolved = _resolve_short_config(central, key, project_root)\n            source = f\"config.{path}\"\n            replacements[token] = resolved\n            input_values[source] = resolved\n        for match in _CONFIG_TOKEN.finditer(content):\n            token, path = match.group(0), match.group(1)\n            source = f\"config.{path}\"\n            resolved = _resolve_config_value(\n                _lookup(central, path, \"config value\"), source, project_root\n            )\n            replacements[token] = resolved\n            input_values[source] = resolved\n        for match in _CUSTOM_TOKEN.finditer(content):\n            if defaults is None:\n                raise RenderError(\"customization tokens require customize.toml\")\n            token, relative_path = match.group(0), match.group(1)\n            path = f\"workflow.{relative_path}\"\n            source = f\"customization.{path}\"\n            resolved, rendered = _resolve_customization_value(\n                _lookup(customization, path, \"customization value\"),\n                _lookup(defaults, path, \"customization default\"),\n                source,\n            )\n            replacements[token] = rendered\n            input_values[source] = resolved\n    return replacements, input_values\n\n\ndef _render_sources(\n    sources: dict[str, str], replacements: dict[str, str], destination: Path\n) -> dict[str, str]:\n    \"\"\"Resolve only tokens authored in installed sources in one opaque pass.\"\"\"\n    # Workflow customization may reference installed skill files; bind those","sourceCodeStart":200,"sourceCodeEnd":236,"githubUrl":"https://github.com/bmad-code-org/BMAD-METHOD/blob/b70486b9bdcb0a404d329e2a763b57964e7f1360/src/scripts/render_skill.py#L200-L236","documentation":"Defensive invariant: a {workflow.*} token in source requires the customization default table (customize.toml). The guard fires only if _CUSTOM_TOKEN matched in a source while defaults is None. Under the normal render() flow this is unreachable, because has_customization (computed by scanning sources for the same token) gates the loading of defaults -- a match implies defaults was loaded. Reaching it indicates a regression or a direct call to the internal _resolve_replacements with defaults=None.","triggerScenarios":"Calling the internal _resolve_replacements(..., None, ...) directly while source content contains {workflow.x}. Not reachable through the public render() entry point, which always loads customize.toml when has_customization is True.","commonSituations":"Third-party code invoking renderer internals directly; a refactor that breaks the has_customization / defaults coupling.","solutions":["Do not call _resolve_replacements directly; use the public render(project_root, skill_dir) entry point.","If you must call it, pass a non-None defaults loaded from customize.toml whenever sources contain {workflow.*} tokens.","Ensure customize.toml exists in the skill directory so defaults can be loaded."],"exampleFix":"# before (direct internal call, defaults=None)\n_resolve_replacements(sources, central, customization, None, project_root)\n\n# after\ndefaults = load_toml(skill_dir / \"customize.toml\", required=True)\n_resolve_replacements(sources, central, customization, defaults, project_root)","handlingStrategy":"try-catch","validationCode":"from pathlib import Path\nimport re\nCUSTOM = re.compile(r\"\\{workflow\\.([A-Za-z0-9_.-]+)\\}\")\n\ndef ensure_customize_toml_if_needed(sources: dict[str,str], skill_dir: Path) -> None:\n    has_custom = any(CUSTOM.search(c) for c in sources.values())\n    if has_custom and not (skill_dir / \"customize.toml\").is_file():\n        raise SystemExit(\"customization tokens present but customize.toml missing\")","typeGuard":null,"tryCatchPattern":"from render_skill import render, RenderError\n\ntry:\n    entry = render(project_root, skill_dir)\nexcept RenderError as e:\n    if \"customization tokens require\" in str(e):\n        # ensure customize.toml exists, then retry via the public entry point\n        ...\n    raise","preventionTips":["Call the public render() entry point, not internal helpers.","Always ship customize.toml alongside any skill whose sources use {workflow.*} tokens.","Don't bypass the has_customization / defaults coupling."],"tags":["python","customization","internal-api","invariant"],"backgroundTag":null,"analyzedSha":"b70486b9bdcb0a404d329e2a763b57964e7f1360","analyzedAt":"2026-08-13T01:21:12.247Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}