fix(quick-dev): normalize render.py paths to forward slashes

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>
This commit is contained in:
Alex Verkhovsky 2026-04-21 23:49:00 -07:00
parent 7428054805
commit ea0c12ac04
1 changed files with 15 additions and 13 deletions

View File

@ -17,6 +17,7 @@ Python 3.11+ stdlib only. UTF-8 I/O.
""" """
import os import os
import posixpath
import re import re
import sys import sys
import tomllib import tomllib
@ -54,8 +55,8 @@ def _deep_merge(base, override):
def load_central_config(root): def load_central_config(root):
"""Four-layer merge of _bmad/config.toml and its peers. HALTs if the base """Four-layer merge of _bmad/config.toml and its peers. HALTs if the base
_bmad/config.toml is absent.""" _bmad/config.toml is absent."""
bmad_dir = os.path.join(root, "_bmad") bmad_dir = posixpath.join(root, "_bmad")
base = os.path.join(bmad_dir, "config.toml") base = posixpath.join(bmad_dir, "config.toml")
if not os.path.isfile(base): if not os.path.isfile(base):
print( print(
f"HALT and report to the user: central config not found at {base}" f"HALT and report to the user: central config not found at {base}"
@ -65,9 +66,9 @@ def load_central_config(root):
layers = [ layers = [
base, base,
os.path.join(bmad_dir, "config.user.toml"), posixpath.join(bmad_dir, "config.user.toml"),
os.path.join(bmad_dir, "custom", "config.toml"), posixpath.join(bmad_dir, "custom", "config.toml"),
os.path.join(bmad_dir, "custom", "config.user.toml"), posixpath.join(bmad_dir, "custom", "config.user.toml"),
] ]
merged = {} merged = {}
for path in layers: for path in layers:
@ -110,7 +111,8 @@ def main():
script_dir = os.path.dirname(os.path.abspath(__file__)) script_dir = os.path.dirname(os.path.abspath(__file__))
skill_name = os.path.basename(script_dir) skill_name = os.path.basename(script_dir)
root = find_project_root() root = find_project_root()
bmad_dir = os.path.join(root, "_bmad") root = root.replace(os.sep, "/")
bmad_dir = posixpath.join(root, "_bmad")
vars_ = flatten_central_config(load_central_config(root)) vars_ = flatten_central_config(load_central_config(root))
@ -118,23 +120,23 @@ def main():
vars_[key] = vars_[key].replace("{project-root}", root) vars_[key] = vars_[key].replace("{project-root}", root)
vars_["project_root"] = root vars_["project_root"] = root
vars_["main_config"] = os.path.join(bmad_dir, "config.toml") vars_["main_config"] = posixpath.join(bmad_dir, "config.toml")
vars_["sprint_status"] = os.path.join( vars_["sprint_status"] = posixpath.join(
vars_["implementation_artifacts"], "sprint-status.yaml" vars_["implementation_artifacts"], "sprint-status.yaml"
) )
vars_["deferred_work_file"] = os.path.join( vars_["deferred_work_file"] = posixpath.join(
vars_["implementation_artifacts"], "deferred-work.md" vars_["implementation_artifacts"], "deferred-work.md"
) )
out_dir = os.path.join(root, "_bmad", "render", skill_name) out_dir = posixpath.join(root, "_bmad", "render", skill_name)
os.makedirs(out_dir, exist_ok=True) os.makedirs(out_dir, exist_ok=True)
count = 0 count = 0
for fname in sorted(os.listdir(script_dir)): for fname in sorted(os.listdir(script_dir)):
if not fname.endswith(".md") or fname == "SKILL.md": if not fname.endswith(".md") or fname == "SKILL.md":
continue continue
src = os.path.join(script_dir, fname) src = posixpath.join(script_dir, fname)
dst = os.path.join(out_dir, fname) dst = posixpath.join(out_dir, fname)
with open(src, "r", encoding="utf-8") as fh: with open(src, "r", encoding="utf-8") as fh:
content = fh.read() content = fh.read()
with open(dst, "w", encoding="utf-8") as fh: with open(dst, "w", encoding="utf-8") as fh:
@ -142,7 +144,7 @@ def main():
count += 1 count += 1
print(f"render.py: rendered {count} files -> {out_dir}", file=sys.stderr) print(f"render.py: rendered {count} files -> {out_dir}", file=sys.stderr)
workflow_md = os.path.join(out_dir, "workflow.md") workflow_md = posixpath.join(out_dir, "workflow.md")
print(f"read and follow {workflow_md}") print(f"read and follow {workflow_md}")