feat(quick-dev): render templates via stdlib Python at skill entry#2281
Draft
feat(quick-dev): render templates via stdlib Python at skill entry#2281
Conversation
b0d7076 to
752c086
Compare
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>
Single happy path: central _bmad/config.toml with four-layer merge, Python 3.11+ required (no ImportError guard), HALT if config missing. Deletes load_flat_yaml, the YAML fallback branch, the setdefault block for planning_artifacts/implementation_artifacts/communication_language, and the tomllib ImportError fallback. Part of plan-quick-dev-python-config-hardening.md (F0). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
On Windows, os.path.join returns backslash-separated paths that can misrender as escape sequences when later concatenated into POSIX shell strings or regexes. Normalize the project root to forward slashes after find_project_root, and use posixpath.join for every path that gets baked into rendered .md files or joined into config values. os.makedirs and os.listdir accept forward-slash paths on Windows, so their call sites stay as-is. Part of plan-quick-dev-python-config-hardening.md (F3). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Python text-mode open() with the platform default performs universal- newline translation: on Windows, LF source files get written as CRLF, producing spurious diffs when rendered output is compared against source. Pass newline="" on both the source read and the rendered write so line endings pass through verbatim. Part of plan-quick-dev-python-config-hardening.md (F4). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
render.py rebuilds from scratch per the docstring, but makedirs(exist_ok=True) only overwrites files that still exist in the source — stale outputs from renamed/deleted source files linger in _bmad/render/bmad-quick-dev/ forever. Remove every .md in the render dir before the render loop; keep the dir itself and any non-.md files. Part of plan-quick-dev-python-config-hardening.md (F5). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The previous INSTALL_ONLY_PATHS entry 'render/' was a blanket prefix
that let every {project-root}/_bmad/render/... reference in any skill
slip past validation. Narrow to 'render/bmad-quick-dev/' so only this
skill's render buffer is whitelisted. Future skills adopting the
stdout-dispatch renderer pattern add their own entries explicitly.
Part of plan-quick-dev-python-config-hardening.md (F6).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
New test/test-quick-dev-renderer.js spins up a temp project with base _bmad/config.toml and a _bmad/custom/config.user.toml override, runs render.py, and asserts the override wins in rendered workflow.md and that sprint_status is rooted at an absolute path in the temp project. Registered as test:renderer in package.json and chained into the npm test script. Part of plan-quick-dev-python-config-hardening.md (F7). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
752c086 to
94cf189
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Move compile-time variable substitution out of the LLM and into a deterministic Python step.
SKILL.mdbecomes a two-line stdout-dispatch shim that runsrender.pyand follows the instruction it prints. The renderer walks up to find_bmad/, loads flat-YAML config, applies smart defaults, bakes{project-root}to an absolute path, and writes rendered.mdfiles to_bmad/render/bmad-quick-dev/.Compile-time refs (
{{.var}}) get substituted at render time. LLM-runtime refs ({var}) pass through untouched.Why
Today the LLM resolves
{project-root},{main_config},{implementation_artifacts},{communication_language},{sprint_status}, and{deferred_work_file}at runtime by reading config. That pushes substitution onto the LLM (fragile: it can misresolve or drift), leaves no place for project-level overrides beyond core BMM config, and conflates compile-time refs with LLM-runtime refs.Moving compile-time substitution into a deterministic Python step eliminates LLM drift and opens the door for other skills to adopt the same pattern later.
How
Renderer (
render.py)find_project_rootwalks up from cwd; HALT to stdout if no_bmad/is found.{{.var}}substitution; unresolved refs emit empty string (Gomissingkey=zerosemantics).planning_artifacts/implementation_artifacts/communication_language. Derivessprint_status/deferred_work_filefromimplementation_artifacts..mdin the skill dir exceptSKILL.md.read and follow {workflow_md}. On failure, stdout HALT directive.Skill entry (
SKILL.md)python render.py, then follow stdout. No template tokens inSKILL.mditself — per the Anthropic skills spec, script stdout is the defined agent-communication channel.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.mduntouched (single-curly comment hint stays as documentation; protected by the new TPL-01 validator rule).Skill-prose cleanups bundled in
spec_file,story_key,diff_output,review_mode) in quick-devstep-01and code-reviewstep-01; empty---/---blocks instep-03/step-05. ThespecLoopIterationcounter init was moved fromstep-04frontmatter into the step body, where first-entry vs loopback semantics are explicit.workflow.mdto the canonical two-line form used bybmad-checkpoint-preview/bmad-create-story/bmad-retrospective:Speak in X. Write any file output in Y.Closes the gap wheredocument_output_languagewas loaded but never enforced on file writes.workflow.mdto its load-bearing parts: drop INITIALIZATION SEQUENCE (only{date}was actually consumed downstream, and it is system-generated rather than config-sourced); drop redundant HALT meta-rules (every step file already has explicit HALT directives at every checkpoint); merge Step Processing Rules with Critical Rules into one list; drop the self-descriptive WORKFLOW ARCHITECTURE intro bullets (Micro-file Design, Just-In-Time Loading, etc.) that did not change LLM behavior. Net: 73 lines down to 38.main_configderivation fromrender.py— nothing in quick-dev still references{{.main_config}}after theworkflow.mdtrim.Tooling
tools/validate-skills.js— add TPL-01 rule. Files whose name containstemplatemust 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. HIGH severity.tools/validate-file-refs.js— addrender/toINSTALL_ONLY_PATHSso the validator recognizes the runtime-generated buffer.tools/skill-validator.md— document TPL-01; deterministic rule count bumped from 14 to 15.Testing
npm ci && npm run qualitypasses — full suite runs via pre-commit hook (lint, markdownlint, test:refs, test:install, format:check, validate:skills). All 242 installation tests pass. Skills validator strict-mode pass — only 2 LOW findings, both in skills not touched by this PR.{...}values, explicit UTF-8 encoding, removal of{{if}}...{{end}}support (nested-case bug; not used by any current template). 10 findings deferred to harness-sidedeferred-work.md.python3 src/bmm-skills/4-implementation/bmad-quick-dev/render.pyfrom a harness cwd produces expected rendered output; all{{.var}}placeholders resolved to absolute paths; runtime{var}refs preserved;spec-template.md's{project-root}/-prefix comment passes through as documentation hint.Follow-ups deferred
_bmad/<module>/config.yamlto TOML. The hand-rolled flat-YAML parser inrender.pyis the cleanest stdlib-Python option today, but it relies on the undocumented invariant that installed configs are flat scalar maps. TOML (tomllib, stdlib 3.11+) is a strict superset of the data model used today and removes the parser entirely. Blast radius and migration plan documented in harnessdeferred-work.md.deferred-work.md.