Skip to content

feat(quick-dev): render templates via stdlib Python at skill entry#2281

Draft
alexeyv wants to merge 7 commits intomainfrom
feat/quick-dev-python-config
Draft

feat(quick-dev): render templates via stdlib Python at skill entry#2281
alexeyv wants to merge 7 commits intomainfrom
feat/quick-dev-python-config

Conversation

@alexeyv
Copy link
Copy Markdown
Collaborator

@alexeyv alexeyv commented Apr 18, 2026

What

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 walks up to find _bmad/, loads flat-YAML config, applies smart defaults, bakes {project-root} to an absolute path, and writes rendered .md files 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)

  • Python 3 stdlib only. 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.
  • Flat-YAML parser skips inline dicts/lists, indented children, and comments. Stderr on missing config; smart defaults applied.
  • {{.var}} substitution; unresolved refs emit empty string (Go missingkey=zero semantics).
  • Smart defaults for planning_artifacts / implementation_artifacts / communication_language. Derives sprint_status / deferred_work_file from implementation_artifacts.
  • Renders every .md in the skill dir except SKILL.md.
  • On success, stderr summary plus a single stdout line: read and follow {workflow_md}. On failure, stdout HALT directive.

Skill entry (SKILL.md)

  • Two-line shim: python render.py, then follow stdout. No template tokens in SKILL.md itself — 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.md untouched (single-curly comment hint stays as documentation; protected by the new TPL-01 validator rule).

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/step-05. The specLoopIteration counter init was 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 to the canonical two-line form used by bmad-checkpoint-preview / bmad-create-story / bmad-retrospective: Speak in X. Write any file output in Y. Closes the gap where document_output_language was loaded but never enforced on file writes.
  • Trim workflow.md to 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.
  • Remove the now-dead main_config derivation from render.py — nothing in quick-dev still references {{.main_config}} after the workflow.md trim.

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. HIGH severity.
  • 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.

Testing

  • npm ci && npm run quality passes — 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.
  • Three adversarial review passes ran in parallel (blind hunter, edge-case hunter, acceptance auditor) at session-capable model tier on the original render.py + template-conversion change. Four patches applied in-branch: stderr on missing config, fix for silent drop of unquoted {...} values, explicit UTF-8 encoding, removal of {{if}}...{{end}} support (nested-case bug; not used by any current template). 10 findings deferred to harness-side deferred-work.md.
  • Skill-prose cleanups (frontmatter, language rule, workflow.md trim) reviewed interactively; no semantic change to runtime behavior — every load-bearing reference traced and confirmed before deletion.
  • Manual: python3 src/bmm-skills/4-implementation/bmad-quick-dev/render.py from 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

  • Migrate _bmad/<module>/config.yaml to TOML. The hand-rolled flat-YAML parser in render.py is 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 harness deferred-work.md.
  • Six other render.py findings (regex hyphen handling, error broadening, atomic writes, etc.) — see harness deferred-work.md.

@alexeyv alexeyv force-pushed the feat/quick-dev-python-config branch from b0d7076 to 752c086 Compare April 20, 2026 17:18
alexeyv and others added 7 commits April 21, 2026 22:10
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>
@alexeyv alexeyv force-pushed the feat/quick-dev-python-config branch from 752c086 to 94cf189 Compare April 22, 2026 07:41
@bmadcode bmadcode closed this Apr 26, 2026
@bmadcode bmadcode reopened this Apr 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants