bmad-method 6.10.1-next.14 → 6.10.1-next.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
|
@@ -12,7 +12,10 @@ files to {project-root}/_bmad/render/bmad-dev-auto/.
|
|
|
12
12
|
Config: four-layer merge of _bmad/config.toml + config.user.toml +
|
|
13
13
|
custom/config.toml + custom/config.user.toml (post-#2285 installs).
|
|
14
14
|
Keys surface from [core] and [modules.bmm]. Missing or unparseable
|
|
15
|
-
config.toml → HALT.
|
|
15
|
+
config.toml → HALT. A {{.var}} referenced by this skill's .md sources but
|
|
16
|
+
absent from the merged config → HALT (never a silent empty substitution).
|
|
17
|
+
Optional layers may be missing, but one that exists and cannot be parsed
|
|
18
|
+
or read → HALT.
|
|
16
19
|
|
|
17
20
|
Customization: three-layer merge of {skill}/customize.toml +
|
|
18
21
|
_bmad/custom/bmad-dev-auto.toml + .user.toml (same structural rules as
|
|
@@ -50,10 +53,12 @@ def find_project_root():
|
|
|
50
53
|
|
|
51
54
|
|
|
52
55
|
def load_toml(path, required=False):
|
|
53
|
-
"""Load a TOML file.
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
56
|
+
"""Load a TOML file. Only absence is negotiable: a missing optional file
|
|
57
|
+
returns {} (customization layers are optional), a missing required file
|
|
58
|
+
HALTs. A file that exists but cannot be parsed or read always HALTs —
|
|
59
|
+
stdout is how this script signals workflow halts to its LLM caller — the
|
|
60
|
+
user wrote it to be honored, and silently continuing with {} would discard
|
|
61
|
+
their customizations with no failure signal."""
|
|
57
62
|
if not os.path.isfile(path):
|
|
58
63
|
if required:
|
|
59
64
|
print(
|
|
@@ -66,17 +71,11 @@ def load_toml(path, required=False):
|
|
|
66
71
|
with open(path, "rb") as fh:
|
|
67
72
|
parsed = tomllib.load(fh)
|
|
68
73
|
except tomllib.TOMLDecodeError as error:
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
sys.exit(1)
|
|
72
|
-
print(f"render.py: warning: failed to parse {path}: {error}", file=sys.stderr)
|
|
73
|
-
return {}
|
|
74
|
+
print(f"HALT and report to the user: failed to parse {path}: {error}")
|
|
75
|
+
sys.exit(1)
|
|
74
76
|
except OSError as error:
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
sys.exit(1)
|
|
78
|
-
print(f"render.py: warning: failed to read {path}: {error}", file=sys.stderr)
|
|
79
|
-
return {}
|
|
77
|
+
print(f"HALT and report to the user: failed to read {path}: {error}")
|
|
78
|
+
sys.exit(1)
|
|
80
79
|
if not isinstance(parsed, dict):
|
|
81
80
|
return {}
|
|
82
81
|
return parsed
|
|
@@ -160,7 +159,7 @@ def resolve_workflow(root, skill_dir, skill_name):
|
|
|
160
159
|
"""Resolve the [workflow] customization block via the three-layer merge
|
|
161
160
|
(skill defaults -> team -> user), highest priority last. Same structural
|
|
162
161
|
rules as resolve_customization.py. All three layers are optional: a missing
|
|
163
|
-
|
|
162
|
+
file is skipped, but an unparseable one HALTs (via load_toml)."""
|
|
164
163
|
defaults = load_toml(posixpath.join(skill_dir, "customize.toml"))
|
|
165
164
|
custom_dir = posixpath.join(root, "_bmad", "custom")
|
|
166
165
|
team = load_toml(posixpath.join(custom_dir, f"{skill_name}.toml"))
|
|
@@ -205,11 +204,27 @@ def flatten_central_config(merged):
|
|
|
205
204
|
|
|
206
205
|
|
|
207
206
|
def render_template(content, vars_):
|
|
208
|
-
"""Resolve {{.var}} substitutions. Unresolved references emit an empty string
|
|
209
|
-
(
|
|
207
|
+
"""Resolve {{.var}} substitutions. Unresolved references emit an empty string,
|
|
208
|
+
but main() HALTs on any missing reference before rendering starts, so this
|
|
209
|
+
fallback never fires in practice."""
|
|
210
210
|
return re.sub(r"\{\{\.(\w+)\}\}", lambda m: vars_.get(m.group(1), ""), content)
|
|
211
211
|
|
|
212
212
|
|
|
213
|
+
def collect_missing_vars(sources, vars_):
|
|
214
|
+
"""Map each {{.var}} name referenced by the source .md files but absent from
|
|
215
|
+
the merged config to the files that reference it. A missing key must HALT:
|
|
216
|
+
missingkey=zero rendering would bake a corrupted workflow (empty paths,
|
|
217
|
+
blank language lines) with no failure signal."""
|
|
218
|
+
missing = {}
|
|
219
|
+
for fname, content in sources:
|
|
220
|
+
for name in re.findall(r"\{\{\.(\w+)\}\}", content):
|
|
221
|
+
if name not in vars_:
|
|
222
|
+
files = missing.setdefault(name, [])
|
|
223
|
+
if fname not in files:
|
|
224
|
+
files.append(fname)
|
|
225
|
+
return missing
|
|
226
|
+
|
|
227
|
+
|
|
213
228
|
def _scalar_str(value):
|
|
214
229
|
"""Stringify a scalar for inline rendering: booleans lowercase (matching
|
|
215
230
|
BMad config conventions), None as empty, everything else via str()."""
|
|
@@ -305,6 +320,9 @@ def main():
|
|
|
305
320
|
|
|
306
321
|
vars_["project_root"] = root
|
|
307
322
|
|
|
323
|
+
# Guarded ahead of the general missing-vars scan: sprint_status and
|
|
324
|
+
# deferred_work_file derive from it below, and unlike the scan (absent
|
|
325
|
+
# keys only) this also HALTs on a present-but-empty value.
|
|
308
326
|
implementation_artifacts = vars_.get("implementation_artifacts", "").strip()
|
|
309
327
|
if not implementation_artifacts:
|
|
310
328
|
print(
|
|
@@ -320,6 +338,27 @@ def main():
|
|
|
320
338
|
implementation_artifacts, "deferred-work.md"
|
|
321
339
|
)
|
|
322
340
|
|
|
341
|
+
sources = []
|
|
342
|
+
for fname in sorted(os.listdir(script_dir)):
|
|
343
|
+
if not fname.endswith(".md") or fname == "SKILL.md":
|
|
344
|
+
continue
|
|
345
|
+
with open(
|
|
346
|
+
posixpath.join(script_dir, fname), "r", encoding="utf-8", newline=""
|
|
347
|
+
) as fh:
|
|
348
|
+
sources.append((fname, fh.read()))
|
|
349
|
+
|
|
350
|
+
missing = collect_missing_vars(sources, vars_)
|
|
351
|
+
if missing:
|
|
352
|
+
details = "; ".join(
|
|
353
|
+
f"`{name}` (referenced by {', '.join(files)})"
|
|
354
|
+
for name, files in sorted(missing.items())
|
|
355
|
+
)
|
|
356
|
+
print(
|
|
357
|
+
f"HALT and report to the user: config is missing {details} "
|
|
358
|
+
"(expected under [core] or [modules.bmm] in _bmad/config.toml)"
|
|
359
|
+
)
|
|
360
|
+
sys.exit(1)
|
|
361
|
+
|
|
323
362
|
workflow = resolve_workflow(root, script_dir.replace(os.sep, "/"), skill_name)
|
|
324
363
|
|
|
325
364
|
out_dir = posixpath.join(root, "_bmad", "render", skill_name)
|
|
@@ -329,13 +368,8 @@ def main():
|
|
|
329
368
|
if fname.endswith(".md"):
|
|
330
369
|
os.remove(posixpath.join(out_dir, fname))
|
|
331
370
|
|
|
332
|
-
for fname in
|
|
333
|
-
if not fname.endswith(".md") or fname == "SKILL.md":
|
|
334
|
-
continue
|
|
335
|
-
src = posixpath.join(script_dir, fname)
|
|
371
|
+
for fname, content in sources:
|
|
336
372
|
dst = posixpath.join(out_dir, fname)
|
|
337
|
-
with open(src, "r", encoding="utf-8", newline="") as fh:
|
|
338
|
-
content = fh.read()
|
|
339
373
|
with open(dst, "w", encoding="utf-8", newline="") as fh:
|
|
340
374
|
fh.write(render_workflow(render_template(content, vars_), workflow))
|
|
341
375
|
|
|
@@ -12,7 +12,10 @@ files to {project-root}/_bmad/render/bmad-quick-dev/.
|
|
|
12
12
|
Config: four-layer merge of _bmad/config.toml + config.user.toml +
|
|
13
13
|
custom/config.toml + custom/config.user.toml (post-#2285 installs).
|
|
14
14
|
Keys surface from [core] and [modules.bmm]. Missing or unparseable
|
|
15
|
-
config.toml → HALT.
|
|
15
|
+
config.toml → HALT. A {{.var}} referenced by this skill's .md sources but
|
|
16
|
+
absent from the merged config → HALT (never a silent empty substitution).
|
|
17
|
+
Optional layers may be missing, but one that exists and cannot be parsed
|
|
18
|
+
or read → HALT.
|
|
16
19
|
|
|
17
20
|
Customization: three-layer merge of {skill}/customize.toml +
|
|
18
21
|
_bmad/custom/bmad-quick-dev.toml + .user.toml (same structural rules as
|
|
@@ -50,10 +53,12 @@ def find_project_root():
|
|
|
50
53
|
|
|
51
54
|
|
|
52
55
|
def load_toml(path, required=False):
|
|
53
|
-
"""Load a TOML file.
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
56
|
+
"""Load a TOML file. Only absence is negotiable: a missing optional file
|
|
57
|
+
returns {} (customization layers are optional), a missing required file
|
|
58
|
+
HALTs. A file that exists but cannot be parsed or read always HALTs —
|
|
59
|
+
stdout is how this script signals workflow halts to its LLM caller — the
|
|
60
|
+
user wrote it to be honored, and silently continuing with {} would discard
|
|
61
|
+
their customizations with no failure signal."""
|
|
57
62
|
if not os.path.isfile(path):
|
|
58
63
|
if required:
|
|
59
64
|
print(
|
|
@@ -66,17 +71,11 @@ def load_toml(path, required=False):
|
|
|
66
71
|
with open(path, "rb") as fh:
|
|
67
72
|
parsed = tomllib.load(fh)
|
|
68
73
|
except tomllib.TOMLDecodeError as error:
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
sys.exit(1)
|
|
72
|
-
print(f"render.py: warning: failed to parse {path}: {error}", file=sys.stderr)
|
|
73
|
-
return {}
|
|
74
|
+
print(f"HALT and report to the user: failed to parse {path}: {error}")
|
|
75
|
+
sys.exit(1)
|
|
74
76
|
except OSError as error:
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
sys.exit(1)
|
|
78
|
-
print(f"render.py: warning: failed to read {path}: {error}", file=sys.stderr)
|
|
79
|
-
return {}
|
|
77
|
+
print(f"HALT and report to the user: failed to read {path}: {error}")
|
|
78
|
+
sys.exit(1)
|
|
80
79
|
if not isinstance(parsed, dict):
|
|
81
80
|
return {}
|
|
82
81
|
return parsed
|
|
@@ -160,7 +159,7 @@ def resolve_workflow(root, skill_dir, skill_name):
|
|
|
160
159
|
"""Resolve the [workflow] customization block via the three-layer merge
|
|
161
160
|
(skill defaults -> team -> user), highest priority last. Same structural
|
|
162
161
|
rules as resolve_customization.py. All three layers are optional: a missing
|
|
163
|
-
|
|
162
|
+
file is skipped, but an unparseable one HALTs (via load_toml)."""
|
|
164
163
|
defaults = load_toml(posixpath.join(skill_dir, "customize.toml"))
|
|
165
164
|
custom_dir = posixpath.join(root, "_bmad", "custom")
|
|
166
165
|
team = load_toml(posixpath.join(custom_dir, f"{skill_name}.toml"))
|
|
@@ -205,11 +204,27 @@ def flatten_central_config(merged):
|
|
|
205
204
|
|
|
206
205
|
|
|
207
206
|
def render_template(content, vars_):
|
|
208
|
-
"""Resolve {{.var}} substitutions. Unresolved references emit an empty string
|
|
209
|
-
(
|
|
207
|
+
"""Resolve {{.var}} substitutions. Unresolved references emit an empty string,
|
|
208
|
+
but main() HALTs on any missing reference before rendering starts, so this
|
|
209
|
+
fallback never fires in practice."""
|
|
210
210
|
return re.sub(r"\{\{\.(\w+)\}\}", lambda m: vars_.get(m.group(1), ""), content)
|
|
211
211
|
|
|
212
212
|
|
|
213
|
+
def collect_missing_vars(sources, vars_):
|
|
214
|
+
"""Map each {{.var}} name referenced by the source .md files but absent from
|
|
215
|
+
the merged config to the files that reference it. A missing key must HALT:
|
|
216
|
+
missingkey=zero rendering would bake a corrupted workflow (empty paths,
|
|
217
|
+
blank language lines) with no failure signal."""
|
|
218
|
+
missing = {}
|
|
219
|
+
for fname, content in sources:
|
|
220
|
+
for name in re.findall(r"\{\{\.(\w+)\}\}", content):
|
|
221
|
+
if name not in vars_:
|
|
222
|
+
files = missing.setdefault(name, [])
|
|
223
|
+
if fname not in files:
|
|
224
|
+
files.append(fname)
|
|
225
|
+
return missing
|
|
226
|
+
|
|
227
|
+
|
|
213
228
|
def _scalar_str(value):
|
|
214
229
|
"""Stringify a scalar for inline rendering: booleans lowercase (matching
|
|
215
230
|
BMad config conventions), None as empty, everything else via str()."""
|
|
@@ -305,6 +320,9 @@ def main():
|
|
|
305
320
|
|
|
306
321
|
vars_["project_root"] = root
|
|
307
322
|
|
|
323
|
+
# Guarded ahead of the general missing-vars scan: sprint_status and
|
|
324
|
+
# deferred_work_file derive from it below, and unlike the scan (absent
|
|
325
|
+
# keys only) this also HALTs on a present-but-empty value.
|
|
308
326
|
implementation_artifacts = vars_.get("implementation_artifacts", "").strip()
|
|
309
327
|
if not implementation_artifacts:
|
|
310
328
|
print(
|
|
@@ -320,6 +338,27 @@ def main():
|
|
|
320
338
|
implementation_artifacts, "deferred-work.md"
|
|
321
339
|
)
|
|
322
340
|
|
|
341
|
+
sources = []
|
|
342
|
+
for fname in sorted(os.listdir(script_dir)):
|
|
343
|
+
if not fname.endswith(".md") or fname == "SKILL.md":
|
|
344
|
+
continue
|
|
345
|
+
with open(
|
|
346
|
+
posixpath.join(script_dir, fname), "r", encoding="utf-8", newline=""
|
|
347
|
+
) as fh:
|
|
348
|
+
sources.append((fname, fh.read()))
|
|
349
|
+
|
|
350
|
+
missing = collect_missing_vars(sources, vars_)
|
|
351
|
+
if missing:
|
|
352
|
+
details = "; ".join(
|
|
353
|
+
f"`{name}` (referenced by {', '.join(files)})"
|
|
354
|
+
for name, files in sorted(missing.items())
|
|
355
|
+
)
|
|
356
|
+
print(
|
|
357
|
+
f"HALT and report to the user: config is missing {details} "
|
|
358
|
+
"(expected under [core] or [modules.bmm] in _bmad/config.toml)"
|
|
359
|
+
)
|
|
360
|
+
sys.exit(1)
|
|
361
|
+
|
|
323
362
|
workflow = resolve_workflow(root, script_dir.replace(os.sep, "/"), skill_name)
|
|
324
363
|
|
|
325
364
|
out_dir = posixpath.join(root, "_bmad", "render", skill_name)
|
|
@@ -329,13 +368,8 @@ def main():
|
|
|
329
368
|
if fname.endswith(".md"):
|
|
330
369
|
os.remove(posixpath.join(out_dir, fname))
|
|
331
370
|
|
|
332
|
-
for fname in
|
|
333
|
-
if not fname.endswith(".md") or fname == "SKILL.md":
|
|
334
|
-
continue
|
|
335
|
-
src = posixpath.join(script_dir, fname)
|
|
371
|
+
for fname, content in sources:
|
|
336
372
|
dst = posixpath.join(out_dir, fname)
|
|
337
|
-
with open(src, "r", encoding="utf-8", newline="") as fh:
|
|
338
|
-
content = fh.read()
|
|
339
373
|
with open(dst, "w", encoding="utf-8", newline="") as fh:
|
|
340
374
|
fh.write(render_workflow(render_template(content, vars_), workflow))
|
|
341
375
|
|