Move compile-time variable substitution out of the LLM and into a
deterministic Python step. SKILL.md becomes a two-line stdout-dispatch
shim that runs render.py and follows the instruction it prints. The
renderer reads BMad configuration from the central four-layer TOML
surface introduced in #2285 (_bmad/config.toml plus config.user.toml
and the two _bmad/custom/ overrides), with a fallback to the legacy
per-module _bmad/bmm/config.yaml for pre-#2285 installs.
Compile-time refs ({{.var}}) get substituted at render time. LLM-runtime
refs ({var}) pass through untouched.
Renderer (render.py)
- Python 3 stdlib only (tomllib, already bundled since 3.11). UTF-8 I/O.
Every invocation rebuilds from scratch — no hash, no cache.
- find_project_root walks up from cwd; HALT to stdout if no _bmad/
is found anywhere on the path.
- load_central_config deep-merges the four TOML layers in priority
order (base-team → base-user → custom-team → custom-user) so user
overrides in _bmad/custom/config.user.toml win over installer-
regenerated base values. flatten_central_config lifts scalar keys
from [core] and [modules.bmm] into the renderer's flat namespace;
module keys beat core on collision (matches the installer's own
core-key-stripping behavior).
- When _bmad/config.toml is absent, falls through to the legacy
flat-YAML parser for _bmad/bmm/config.yaml — the renderer keeps
working across the #2285 transition.
- {{.var}} substitution; unresolved refs emit empty string (Go
missingkey=zero semantics).
- Smart defaults for planning_artifacts / implementation_artifacts /
communication_language applied after config load. Derives
sprint_status / deferred_work_file from implementation_artifacts.
{{.main_config}} points at whichever surface was actually read.
- Renders every .md in the skill dir except SKILL.md to
{project-root}/_bmad/render/bmad-quick-dev/.
- On success, stderr summary plus a single stdout line:
"read and follow {workflow_md}". On failure, stdout HALT directive —
per the Anthropic skills spec, script stdout is the defined agent-
communication channel.
Skill entry (SKILL.md)
- Two-line shim: run python render.py, follow stdout. No template
tokens in SKILL.md itself.
Template conversions
- workflow.md, step-01..05, step-oneshot, sync-sprint-status: convert
every compile-time {var} reference to {{.var}}. Runtime refs
preserved.
- spec-template.md untouched (single-curly comment hint stays as
documentation).
Skill-prose cleanups bundled in
- Remove dead step-file frontmatter: empty-string variable declarations
(spec_file, story_key, diff_output, review_mode) in quick-dev step-01
and code-review step-01; empty --- --- blocks in step-03 and step-05;
the specLoopIteration counter init moved from step-04 frontmatter into
the step body where first-entry vs loopback semantics are explicit.
- Unify the language rule across all six quick-dev step files plus
workflow.md.
Tooling
- tools/validate-skills.js: add TPL-01 rule. Files whose name contains
"template" must not contain compile-time {{.var}} substitutions.
Template files seed durable, version-controlled artifacts that
execute on other machines; baking a value at render time would
freeze a machine-local path into every downstream artifact.
- tools/validate-file-refs.js: add render/ to INSTALL_ONLY_PATHS so
the validator recognizes the runtime-generated buffer.
- tools/skill-validator.md: document TPL-01; deterministic rule count
bumped from 14 to 15.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>