mirror of
https://github.com/bmad-code-org/BMAD-METHOD.git
synced 2026-08-28 19:20:41 +08:00
fix(dev-auto): temporarily revert skill-entry renderer (#2598)
Back out the dev-auto render.py workflow entry change from #2587, including the follow-up renderer-only fix on top of it, so the skill returns to the pre-renderer SKILL.md flow while the longer fixes are worked separately.
This commit is contained in:
@@ -74,7 +74,7 @@ Exactly one `stories.yaml` entry is dispatched per invocation: the workflow neve
|
||||
|
||||
On activation, the workflow resolves:
|
||||
|
||||
- BMad configuration from `_bmad/config.toml` and its override layers
|
||||
- `_bmad/bmm/config.yaml`
|
||||
- Any configured workflow customizations from `customize.toml`, team overrides, and user overrides
|
||||
- Persistent facts listed in workflow config
|
||||
- `project-context.md` files, if present
|
||||
|
||||
@@ -74,7 +74,7 @@ workflow 读取 `<spec-folder>/stories.yaml`,查找 `id` 匹配的条目。它
|
||||
|
||||
激活时,workflow 解析:
|
||||
|
||||
- `_bmad/config.toml` 及其 override 层中的 BMad 配置
|
||||
- `_bmad/bmm/config.yaml`
|
||||
- `customize.toml`、团队 override、用户 override 中的 workflow 自定义
|
||||
- workflow 配置中列出的 persistent facts
|
||||
- 若存在的 `project-context.md` 文件
|
||||
|
||||
+1
-1
@@ -46,7 +46,7 @@
|
||||
"test:channels": "node test/test-installer-channels.js",
|
||||
"test:install": "node test/test-installation-components.js",
|
||||
"test:refs": "node test/test-file-refs-csv.js",
|
||||
"test:renderer": "node test/test-quick-dev-renderer.js && node test/test-dev-auto-renderer.js",
|
||||
"test:renderer": "node test/test-quick-dev-renderer.js",
|
||||
"test:skills": "node test/test-validate-skills.js",
|
||||
"test:urls": "node test/test-parse-source-urls.js",
|
||||
"validate:refs": "node tools/validate-file-refs.js --strict",
|
||||
|
||||
@@ -3,11 +3,121 @@ name: bmad-dev-auto
|
||||
description: 'One iteration of an unattended development loop. Use when invoked by name.'
|
||||
---
|
||||
|
||||
Run this, substituting `{skill-root}` with the absolute path to this skill's base directory, without changing the cwd:
|
||||
# Dev Auto Workflow
|
||||
|
||||
```bash
|
||||
uv run {skill-root}/render.py
|
||||
```
|
||||
**Goal:** Turn intent into a hardened, reviewable artifact, without human interaction.
|
||||
|
||||
- **On success:** follow the instruction it prints to stdout; ignore stderr.
|
||||
- **On any failure** (including `uv` not being installed): report what it printed and HALT.
|
||||
**CRITICAL:** If a step says "read fully and follow step-XX", you read and follow step-XX. No exceptions.
|
||||
|
||||
## HALT
|
||||
|
||||
To HALT with a final status and optional blocking condition:
|
||||
|
||||
1. **Folder+id dispatch** (`{spec_folder}` and `{story_id}` are set): the write-back always lands at the id-keyed story spec. The `{implementation_artifacts}` fallback in step 2 below is never used in this mode, even for halts before planning starts.
|
||||
- If `{spec_file}` is still empty, resolve it now:
|
||||
- **Entry not resolved** (`stories.yaml` is missing/unparseable, or `{story_id}` has no matching entry): use the fixed slug segment `unresolved`: `{spec_file}` = `{spec_folder}/stories/{story_id}-unresolved.md`.
|
||||
- **Ambiguous on-disk match** (the halt is `ambiguous story file match` — more than one file already matches `{spec_folder}/stories/{story_id}-*.md`): use the fixed slug segment `ambiguous` instead of deriving from the title, so the write-back neither creates a third title-derived candidate nor risks silently landing on one of the existing ambiguous files: `{spec_file}` = `{spec_folder}/stories/{story_id}-ambiguous.md`.
|
||||
- **Otherwise** (the entry was resolved and no ambiguous on-disk match exists): derive `{spec_file}` = `{spec_folder}/stories/{story_id}-{slug}.md`, where `{slug}` is a kebab-case slug from `title` (and `description` if needed) with no `{story_id}` prefix — the same derivation step-01's Route uses.
|
||||
- If `{spec_file}` exists on disk, update `status` in frontmatter and append missing result details under `## Auto Run Result`.
|
||||
- If it does not exist, create it as a skeletal story spec:
|
||||
```markdown
|
||||
---
|
||||
status: <final status>
|
||||
---
|
||||
|
||||
# <entry title, or "Story {story_id}" if the entry could not be resolved or the on-disk match was ambiguous>
|
||||
|
||||
## Auto Run Result
|
||||
|
||||
Status: <final status>
|
||||
Blocking condition: <blocking condition, if any>
|
||||
```
|
||||
2. **Otherwise:**
|
||||
- If `{spec_file}` is known and exists, update `status` in frontmatter and append missing result details under `## Auto Run Result`.
|
||||
- If `{spec_file}` is unknown or missing, create `{implementation_artifacts}/bmad-dev-auto-result-<slug-or-timestamp>.md` with:
|
||||
```markdown
|
||||
---
|
||||
status: <final status>
|
||||
---
|
||||
|
||||
# BMad Dev Auto Result
|
||||
|
||||
Status: <final status>
|
||||
Blocking condition: <blocking condition, if any>
|
||||
```
|
||||
3. Run: `python3 {project-root}/_bmad/scripts/resolve_customization.py --skill {skill-root} --key workflow.on_complete`
|
||||
4. If the resolved `workflow.on_complete` is non-empty, follow it as the final instruction before exiting.
|
||||
5. Stop the workflow.
|
||||
|
||||
## Subagents
|
||||
|
||||
Using subagents when instructed is mandatory. If you cannot, HALT with status `blocked` and blocking condition `no subagents`.
|
||||
|
||||
Invoke every subagent **synchronously**: launch it, wait for it to return within the same turn, then continue with its result. When a step says to run subagents "in parallel" (e.g. the reviewers), that means several **blocking** calls awaited together in one turn — not detached execution. Never run a subagent in the background / detached / async (e.g. `run_in_background: true`), and never end your turn to "await a completion notification." This workflow runs unattended: there is no event loop to resume a yielded turn, so a backgrounded subagent never hands control back and the run stalls. The only sanctioned way to end a turn is the HALT protocol above with an explicit terminal `status`.
|
||||
|
||||
## READY FOR DEVELOPMENT STANDARD
|
||||
|
||||
A specification is "Ready for Development" when:
|
||||
|
||||
- **Actionable**: Every task has a file path and specific action.
|
||||
- **Logical**: Tasks ordered by dependency.
|
||||
- **Testable**: All ACs use Given/When/Then.
|
||||
- **Surface-anchored**: ACs observe the outermost surface the intent references — never a more internal proxy for it (e.g. the API response, not the database row behind it).
|
||||
- **Complete**: No placeholders or TBDs.
|
||||
- **Sufficient**: No known requirement, acceptance, dependency, or implementation gaps remain unresolved.
|
||||
- **Coherent**: No unresolved ambiguities or internal contradictions.
|
||||
|
||||
## Conventions
|
||||
|
||||
- Bare paths (e.g. `step-01-clarify-and-route.md`) resolve from the skill root.
|
||||
- `{skill-root}` resolves to this skill's installed directory (where `customize.toml` lives).
|
||||
- `{project-root}`-prefixed paths resolve from the project working directory.
|
||||
- `{skill-name}` resolves to the skill directory's basename.
|
||||
|
||||
## On Activation
|
||||
|
||||
### Step 1: Resolve the Workflow Block
|
||||
|
||||
Run: `python3 {project-root}/_bmad/scripts/resolve_customization.py --skill {skill-root} --key workflow`
|
||||
|
||||
**If the script fails**, resolve the `workflow` block yourself by reading these three files in base → team → user order and applying the same structural merge rules as the resolver:
|
||||
|
||||
1. `{skill-root}/customize.toml` — defaults
|
||||
2. `{project-root}/_bmad/custom/{skill-name}.toml` — team overrides
|
||||
3. `{project-root}/_bmad/custom/{skill-name}.user.toml` — personal overrides
|
||||
|
||||
Any missing file is skipped. Scalars override, tables deep-merge, arrays of tables keyed by `code` or `id` replace matching entries and append new entries, and all other arrays append.
|
||||
|
||||
### Step 2: Execute Prepend Steps
|
||||
|
||||
Execute each entry in `{workflow.activation_steps_prepend}` in order before proceeding.
|
||||
|
||||
### Step 3: Load Persistent Facts
|
||||
|
||||
Treat every entry in `{workflow.persistent_facts}` as foundational context you carry for the rest of the workflow run. Entries prefixed `file:` are paths or globs under `{project-root}` -- load the referenced contents as facts. All other entries are facts verbatim.
|
||||
|
||||
### Step 4: Load Config
|
||||
|
||||
Load config from `{project-root}/_bmad/bmm/config.yaml` and resolve:
|
||||
|
||||
- `project_name`, `planning_artifacts`, `implementation_artifacts`, `user_name`
|
||||
- `communication_language`, `document_output_language`, `user_skill_level`
|
||||
- `date` as system-generated current datetime
|
||||
- `project_context` = `**/project-context.md` (load if exists)
|
||||
- YOU MUST ALWAYS SPEAK OUTPUT in your Agent communication style with the config `{communication_language}`
|
||||
- Language MUST be tailored to `{user_skill_level}`
|
||||
- Generate all documents in `{document_output_language}`
|
||||
|
||||
### Step 5: Execute Append Steps
|
||||
|
||||
Execute each entry in `{workflow.activation_steps_append}` in order.
|
||||
|
||||
Activation is complete after all activation steps have run.
|
||||
|
||||
## Workflow Execution
|
||||
|
||||
Follow the step files in order. Read one step fully, execute it, then load the next step only when directed. Do not skip, reorder, or pre-load steps.
|
||||
|
||||
## First workflow step
|
||||
|
||||
Read fully and follow: `./step-01-clarify-and-route.md` to begin the workflow.
|
||||
|
||||
@@ -1,381 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# /// script
|
||||
# requires-python = ">=3.11"
|
||||
# ///
|
||||
"""render.py — bmad-dev-auto template renderer.
|
||||
|
||||
Resolves compile-time {{.variable}} placeholders from BMad's central config,
|
||||
bakes absolute paths for {project-root} into derived values, resolves and
|
||||
inlines the skill's [workflow] customization block, and writes rendered .md
|
||||
files to {project-root}/_bmad/render/bmad-dev-auto/.
|
||||
|
||||
Config: four-layer merge of _bmad/config.toml + config.user.toml +
|
||||
custom/config.toml + custom/config.user.toml (post-#2285 installs).
|
||||
Keys surface from [core] and [modules.bmm]. Missing or unparseable
|
||||
config.toml → HALT. A {{.var}} referenced by this skill's .md sources but
|
||||
absent from the merged config → HALT (never a silent empty substitution).
|
||||
Optional layers may be missing, but one that exists and cannot be parsed
|
||||
or read → HALT.
|
||||
|
||||
Customization: three-layer merge of {skill}/customize.toml +
|
||||
_bmad/custom/bmad-dev-auto.toml + .user.toml (same structural rules as
|
||||
resolve_customization.py). The resolved [workflow] values fill {workflow.*}
|
||||
placeholders, so this skill needs no runtime resolve_customization.py call.
|
||||
Other single-curly placeholders ({project-root}, {spec_file}, {skill-root},
|
||||
...) pass through untouched for the LLM to resolve during workflow execution.
|
||||
|
||||
Every invocation rebuilds from scratch — no hash, no cache.
|
||||
Python 3.11+ stdlib only. UTF-8 I/O.
|
||||
"""
|
||||
|
||||
import os
|
||||
import posixpath
|
||||
import re
|
||||
import sys
|
||||
import tomllib
|
||||
|
||||
|
||||
def find_project_root():
|
||||
"""Walk up from cwd until a _bmad/ directory is found. On failure, print a
|
||||
HALT instruction to stdout and exit non-zero."""
|
||||
current = os.path.abspath(os.getcwd())
|
||||
while True:
|
||||
candidate = os.path.join(current, "_bmad")
|
||||
if os.path.isdir(candidate):
|
||||
return current
|
||||
parent = os.path.dirname(current)
|
||||
if parent == current:
|
||||
print(
|
||||
f"HALT and report to the user: no _bmad/ directory found walking up from {os.getcwd()}"
|
||||
)
|
||||
sys.exit(1)
|
||||
current = parent
|
||||
|
||||
|
||||
def load_toml(path, required=False):
|
||||
"""Load a TOML file. Only absence is negotiable: a missing optional file
|
||||
returns {} (customization layers are optional), a missing required file
|
||||
HALTs. A file that exists but cannot be parsed or read always HALTs —
|
||||
stdout is how this script signals workflow halts to its LLM caller — the
|
||||
user wrote it to be honored, and silently continuing with {} would discard
|
||||
their customizations with no failure signal."""
|
||||
if not os.path.isfile(path):
|
||||
if required:
|
||||
print(
|
||||
f"HALT and report to the user: required config file not found: {path} — "
|
||||
"ensure this is a post-#2285 BMAD install"
|
||||
)
|
||||
sys.exit(1)
|
||||
return {}
|
||||
try:
|
||||
with open(path, "rb") as fh:
|
||||
parsed = tomllib.load(fh)
|
||||
except tomllib.TOMLDecodeError as error:
|
||||
print(f"HALT and report to the user: failed to parse {path}: {error}")
|
||||
sys.exit(1)
|
||||
except OSError as error:
|
||||
print(f"HALT and report to the user: failed to read {path}: {error}")
|
||||
sys.exit(1)
|
||||
if not isinstance(parsed, dict):
|
||||
return {}
|
||||
return parsed
|
||||
|
||||
|
||||
def _deep_merge(base, override):
|
||||
"""Dict-aware deep merge. Lists and scalars: override wins (we don't need
|
||||
the full keyed-merge semantics of resolve_config.py — dev-auto only reads
|
||||
flat scalars out of [core] and [modules.bmm])."""
|
||||
if isinstance(base, dict) and isinstance(override, dict):
|
||||
result = dict(base)
|
||||
for key, value in override.items():
|
||||
result[key] = _deep_merge(result[key], value) if key in result else value
|
||||
return result
|
||||
return override
|
||||
|
||||
|
||||
def _detect_keyed_merge_field(items):
|
||||
"""Return 'code' or 'id' if every table item carries that same field.
|
||||
Mixed or partial arrays return None and fall through to append."""
|
||||
if not items or not all(isinstance(item, dict) for item in items):
|
||||
return None
|
||||
for candidate in ("code", "id"):
|
||||
if all(item.get(candidate) is not None for item in items):
|
||||
return candidate
|
||||
return None
|
||||
|
||||
|
||||
def _merge_by_key(base, override, key_name):
|
||||
result = []
|
||||
index_by_key = {}
|
||||
for item in base:
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
if item.get(key_name) is not None:
|
||||
index_by_key[item[key_name]] = len(result)
|
||||
result.append(dict(item))
|
||||
for item in override:
|
||||
if not isinstance(item, dict):
|
||||
result.append(item)
|
||||
continue
|
||||
key = item.get(key_name)
|
||||
if key is not None and key in index_by_key:
|
||||
result[index_by_key[key]] = dict(item)
|
||||
else:
|
||||
if key is not None:
|
||||
index_by_key[key] = len(result)
|
||||
result.append(dict(item))
|
||||
return result
|
||||
|
||||
|
||||
def _merge_arrays(base, override):
|
||||
"""Shape-aware array merge: keyed merge if every item has code/id, else append."""
|
||||
base_arr = base if isinstance(base, list) else []
|
||||
override_arr = override if isinstance(override, list) else []
|
||||
keyed_field = _detect_keyed_merge_field(base_arr + override_arr)
|
||||
if keyed_field:
|
||||
return _merge_by_key(base_arr, override_arr, keyed_field)
|
||||
return base_arr + override_arr
|
||||
|
||||
|
||||
def _structural_merge(base, override):
|
||||
"""Faithful port of resolve_customization.py's deep_merge: tables deep-merge,
|
||||
arrays-of-tables keyed by code/id replace-then-append (other arrays append),
|
||||
scalars override. Used only for the [workflow] customization layers — the
|
||||
central-config path keeps its own simpler _deep_merge. Duplicated rather than
|
||||
imported to keep this skill self-contained."""
|
||||
if isinstance(base, dict) and isinstance(override, dict):
|
||||
result = dict(base)
|
||||
for key, over_val in override.items():
|
||||
result[key] = (
|
||||
_structural_merge(result[key], over_val) if key in result else over_val
|
||||
)
|
||||
return result
|
||||
if isinstance(base, list) and isinstance(override, list):
|
||||
return _merge_arrays(base, override)
|
||||
return override
|
||||
|
||||
|
||||
def resolve_workflow(root, skill_dir, skill_name):
|
||||
"""Resolve the [workflow] customization block via the three-layer merge
|
||||
(skill defaults -> team -> user), highest priority last. Same structural
|
||||
rules as resolve_customization.py. All three layers are optional: a missing
|
||||
file is skipped, but an unparseable one HALTs (via load_toml)."""
|
||||
defaults = load_toml(posixpath.join(skill_dir, "customize.toml"))
|
||||
custom_dir = posixpath.join(root, "_bmad", "custom")
|
||||
team = load_toml(posixpath.join(custom_dir, f"{skill_name}.toml"))
|
||||
user = load_toml(posixpath.join(custom_dir, f"{skill_name}.user.toml"))
|
||||
merged = _structural_merge(defaults, team)
|
||||
merged = _structural_merge(merged, user)
|
||||
workflow = merged.get("workflow")
|
||||
return workflow if isinstance(workflow, dict) else {}
|
||||
|
||||
|
||||
def load_central_config(root):
|
||||
"""Four-layer merge of _bmad/config.toml and its peers (highest priority
|
||||
last). HALTs if the base _bmad/config.toml is missing or unparseable."""
|
||||
bmad_dir = posixpath.join(root, "_bmad")
|
||||
base_team = load_toml(posixpath.join(bmad_dir, "config.toml"), required=True)
|
||||
base_user = load_toml(posixpath.join(bmad_dir, "config.user.toml"))
|
||||
custom_team = load_toml(posixpath.join(bmad_dir, "custom", "config.toml"))
|
||||
custom_user = load_toml(posixpath.join(bmad_dir, "custom", "config.user.toml"))
|
||||
|
||||
merged = _deep_merge(base_team, base_user)
|
||||
merged = _deep_merge(merged, custom_team)
|
||||
merged = _deep_merge(merged, custom_user)
|
||||
return merged
|
||||
|
||||
|
||||
def flatten_central_config(merged):
|
||||
"""Lift scalar keys from [core] and [modules.bmm] into a single namespace.
|
||||
Module keys take precedence on collision (installer strips core keys from
|
||||
module buckets, so collisions shouldn't happen in practice)."""
|
||||
flat = {}
|
||||
modules = merged.get("modules")
|
||||
modules = modules if isinstance(modules, dict) else {}
|
||||
for section in (merged.get("core"), modules.get("bmm")):
|
||||
if not isinstance(section, dict):
|
||||
continue
|
||||
for key, value in section.items():
|
||||
if isinstance(value, bool):
|
||||
flat[key] = "true" if value else "false"
|
||||
elif isinstance(value, (str, int, float)):
|
||||
flat[key] = str(value)
|
||||
return flat
|
||||
|
||||
|
||||
def render_template(content, vars_):
|
||||
"""Resolve {{.var}} substitutions. Unresolved references emit an empty string,
|
||||
but main() HALTs on any missing reference before rendering starts, so this
|
||||
fallback never fires in practice."""
|
||||
return re.sub(r"\{\{\.(\w+)\}\}", lambda m: vars_.get(m.group(1), ""), content)
|
||||
|
||||
|
||||
def collect_missing_vars(sources, vars_):
|
||||
"""Map each {{.var}} name referenced by the source .md files but absent from
|
||||
the merged config to the files that reference it. A missing key must HALT:
|
||||
missingkey=zero rendering would bake a corrupted workflow (empty paths,
|
||||
blank language lines) with no failure signal."""
|
||||
missing = {}
|
||||
for fname, content in sources:
|
||||
for name in re.findall(r"\{\{\.(\w+)\}\}", content):
|
||||
if name not in vars_:
|
||||
files = missing.setdefault(name, [])
|
||||
if fname not in files:
|
||||
files.append(fname)
|
||||
return missing
|
||||
|
||||
|
||||
def _scalar_str(value):
|
||||
"""Stringify a scalar for inline rendering: booleans lowercase (matching
|
||||
BMad config conventions), None as empty, everything else via str()."""
|
||||
if value is None:
|
||||
return ""
|
||||
if isinstance(value, bool):
|
||||
return "true" if value else "false"
|
||||
return str(value)
|
||||
|
||||
|
||||
# [workflow] keys holding review layers ([[workflow.review_layers]] tables with
|
||||
# id/name/instruction/when fields). This renderer knows this skill's
|
||||
# customization schema outright — layer semantics are materialized here, not
|
||||
# interpreted by the LLM at run time.
|
||||
_REVIEW_LAYER_KEYS = ("review_layers", "oneshot_review_layers")
|
||||
|
||||
|
||||
def _render_review_layers(layers):
|
||||
"""Materialize review layers into direct invocation blocks. A layer with an
|
||||
empty or missing instruction is disabled (that is how an override turns off
|
||||
a default layer) and drops out entirely. A `when` condition is the one part
|
||||
that stays with the LLM: it renders as a run-time guard line. No active
|
||||
layers renders as the HALT instruction the workflow would otherwise have to
|
||||
derive from an empty list."""
|
||||
active = [
|
||||
layer
|
||||
for layer in layers
|
||||
if isinstance(layer, dict) and _scalar_str(layer.get("instruction")).strip()
|
||||
]
|
||||
if not active:
|
||||
return (
|
||||
"No review layers are active. HALT with status `blocked` and "
|
||||
"blocking condition `no active review layers`."
|
||||
)
|
||||
blocks = []
|
||||
for layer in active:
|
||||
title = (
|
||||
_scalar_str(layer.get("name")).strip()
|
||||
or _scalar_str(layer.get("id")).strip()
|
||||
or "Review layer"
|
||||
)
|
||||
lines = [f"#### {title}", ""]
|
||||
when = _scalar_str(layer.get("when")).strip()
|
||||
if when:
|
||||
lines.append(
|
||||
"Run this layer only if the following holds in the "
|
||||
f"current context: `{when}`"
|
||||
)
|
||||
lines.append("")
|
||||
lines.append(_scalar_str(layer.get("instruction")).strip("\n"))
|
||||
blocks.append("\n".join(lines))
|
||||
return "\n\n".join(blocks)
|
||||
|
||||
|
||||
def _render_workflow_value(key, value):
|
||||
"""Format a resolved [workflow] value for inline substitution. Review-layer
|
||||
keys materialize as invocation blocks; other lists render as markdown
|
||||
bullets (empty -> '_None._'); scalars render verbatim. Each list item uses
|
||||
the same scalar formatting so booleans stay consistent. Entries are emitted
|
||||
as-is so runtime placeholders like {project-root} or {diff_output} survive
|
||||
for the LLM to resolve."""
|
||||
if key in _REVIEW_LAYER_KEYS and isinstance(value, list):
|
||||
return _render_review_layers(value)
|
||||
if isinstance(value, list):
|
||||
if not value:
|
||||
return "_None._"
|
||||
return "\n".join(f"- {_scalar_str(item)}" for item in value)
|
||||
return _scalar_str(value)
|
||||
|
||||
|
||||
def render_workflow(content, workflow):
|
||||
"""Resolve {workflow.<key>} placeholders from the resolved [workflow] block.
|
||||
Unknown keys emit an empty string (missingkey=zero, matching render_template).
|
||||
Distinct regex from render_template so single-curly runtime placeholders
|
||||
elsewhere are untouched."""
|
||||
return re.sub(
|
||||
r"\{workflow\.(\w+)\}",
|
||||
lambda m: _render_workflow_value(m.group(1), workflow.get(m.group(1))),
|
||||
content,
|
||||
)
|
||||
|
||||
|
||||
def main():
|
||||
script_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
skill_name = os.path.basename(script_dir)
|
||||
root = find_project_root()
|
||||
root = root.replace(os.sep, "/")
|
||||
|
||||
vars_ = flatten_central_config(load_central_config(root))
|
||||
|
||||
for key in list(vars_.keys()):
|
||||
vars_[key] = vars_[key].replace("{project-root}", root)
|
||||
|
||||
vars_["project_root"] = root
|
||||
|
||||
# Guarded ahead of the general missing-vars scan: sprint_status and
|
||||
# deferred_work_file derive from it below, and unlike the scan (absent
|
||||
# keys only) this also HALTs on a present-but-empty value.
|
||||
implementation_artifacts = vars_.get("implementation_artifacts", "").strip()
|
||||
if not implementation_artifacts:
|
||||
print(
|
||||
"HALT and report to the user: config is missing `implementation_artifacts` "
|
||||
"(expected under [core] or [modules.bmm] in _bmad/config.toml)"
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
vars_["sprint_status"] = posixpath.join(
|
||||
implementation_artifacts, "sprint-status.yaml"
|
||||
)
|
||||
vars_["deferred_work_file"] = posixpath.join(
|
||||
implementation_artifacts, "deferred-work.md"
|
||||
)
|
||||
|
||||
sources = []
|
||||
for fname in sorted(os.listdir(script_dir)):
|
||||
if not fname.endswith(".md") or fname == "SKILL.md":
|
||||
continue
|
||||
with open(
|
||||
posixpath.join(script_dir, fname), "r", encoding="utf-8", newline=""
|
||||
) as fh:
|
||||
sources.append((fname, fh.read()))
|
||||
|
||||
missing = collect_missing_vars(sources, vars_)
|
||||
if missing:
|
||||
details = "; ".join(
|
||||
f"`{name}` (referenced by {', '.join(files)})"
|
||||
for name, files in sorted(missing.items())
|
||||
)
|
||||
print(
|
||||
f"HALT and report to the user: config is missing {details} "
|
||||
"(expected under [core] or [modules.bmm] in _bmad/config.toml)"
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
workflow = resolve_workflow(root, script_dir.replace(os.sep, "/"), skill_name)
|
||||
|
||||
out_dir = posixpath.join(root, "_bmad", "render", skill_name)
|
||||
os.makedirs(out_dir, exist_ok=True)
|
||||
|
||||
for fname in os.listdir(out_dir):
|
||||
if fname.endswith(".md"):
|
||||
os.remove(posixpath.join(out_dir, fname))
|
||||
|
||||
for fname, content in sources:
|
||||
dst = posixpath.join(out_dir, fname)
|
||||
with open(dst, "w", encoding="utf-8", newline="") as fh:
|
||||
fh.write(render_workflow(render_template(content, vars_), workflow))
|
||||
|
||||
workflow_md = posixpath.join(out_dir, "workflow.md")
|
||||
print(f"read and follow {workflow_md}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,4 +1,5 @@
|
||||
---
|
||||
deferred_work_file: '{implementation_artifacts}/deferred-work.md'
|
||||
spec_file: '' # set at runtime once a route resolves it; some HALT branches exit before it is set
|
||||
spec_folder: '' # set at runtime under folder+id dispatch only
|
||||
story_id: '' # set at runtime under folder+id dispatch only
|
||||
@@ -8,7 +9,7 @@ story_id: '' # set at runtime under folder+id dispatch only
|
||||
|
||||
## RULES
|
||||
|
||||
- **Language** — Speak in `{{.communication_language}}`. Write any file output in `{{.document_output_language}}`.
|
||||
- YOU MUST ALWAYS SPEAK OUTPUT in your Agent communication style with the config `{communication_language}`
|
||||
- Treat the invocation intent as workflow input, not as a substitute for step-02 investigation and spec generation.
|
||||
- **EARLY EXIT** means: stop this step immediately, then read and follow the target file. Return here only if a later step explicitly says to loop back.
|
||||
|
||||
@@ -43,7 +44,7 @@ If the invocation prompt does not contain enough intent to identify what to impl
|
||||
## INSTRUCTIONS
|
||||
|
||||
1. Load context.
|
||||
- List files in `{{.planning_artifacts}}` and `{{.implementation_artifacts}}`.
|
||||
- List files in `{planning_artifacts}` and `{implementation_artifacts}`.
|
||||
- If the invocation prompt points to an unformatted spec or intent file, ingest that file. Do not scan for unrelated intent files.
|
||||
- **Determine context strategy.** Using the intent and the artifact listing, infer whether the current work is a story from an epic. Do not rely on filename patterns or regex — reason about the intent, the listing, and any epics file content together.
|
||||
|
||||
@@ -51,15 +52,15 @@ If the invocation prompt does not contain enough intent to identify what to impl
|
||||
|
||||
1. Identify the epic number `{epic_num}` and (if present) the story number `{story_num}`. If you can't identify an epic number, use path B.
|
||||
|
||||
2. **Check for a valid cached epic context.** Look for `{{.implementation_artifacts}}/epic-<N>-context.md` (where `<N>` is the epic number). A file is **valid** when it exists, is non-empty, starts with `# Epic <N> Context:` (with the correct epic number), and no file in `{{.planning_artifacts}}` is newer.
|
||||
2. **Check for a valid cached epic context.** Look for `{implementation_artifacts}/epic-<N>-context.md` (where `<N>` is the epic number). A file is **valid** when it exists, is non-empty, starts with `# Epic <N> Context:` (with the correct epic number), and no file in `{planning_artifacts}` is newer.
|
||||
- **If valid:** load it as the primary planning context. Do not load raw planning docs (PRD, architecture, UX, etc.).
|
||||
- **If missing, empty, or invalid:** compile it in the next bullet.
|
||||
|
||||
3. **Compile epic context if needed.** If no valid cached epic context was loaded, produce `{{.implementation_artifacts}}/epic-<N>-context.md` by spawning a subagent synchronously (wait for it to return in this turn) with `./compile-epic-context.md` as its prompt. Pass it the epic number, the epics file path, the `{{.planning_artifacts}}` directory, and the output path `{{.implementation_artifacts}}/epic-<N>-context.md`.
|
||||
3. **Compile epic context if needed.** If no valid cached epic context was loaded, produce `{implementation_artifacts}/epic-<N>-context.md` by spawning a subagent synchronously (wait for it to return in this turn) with `./compile-epic-context.md` as its prompt. Pass it the epic number, the epics file path, the `{planning_artifacts}` directory, and the output path `{implementation_artifacts}/epic-<N>-context.md`.
|
||||
|
||||
4. **Verify if compiled.** If epic context was compiled, verify the output file exists, is non-empty, and starts with `# Epic <N> Context:`. If valid, load it. If verification fails, HALT with status `blocked` and blocking condition `context compilation verification failed`.
|
||||
|
||||
5. **Previous story continuity.** Regardless of which context source succeeded above, scan `{{.implementation_artifacts}}` for specs from the same epic with `status: done` and a lower story number. Load the most recent one (highest story number below current). Extract its **Code Map**, **Design Notes**, **Spec Change Log**, and **task list** as continuity context for step-02 planning. If no `done` spec is found but an `in-review` spec exists for the same epic with a lower story number, HALT with status `blocked` and blocking condition `missing previous-story continuity decision`.
|
||||
5. **Previous story continuity.** Regardless of which context source succeeded above, scan `{implementation_artifacts}` for specs from the same epic with `status: done` and a lower story number. Load the most recent one (highest story number below current). Extract its **Code Map**, **Design Notes**, **Spec Change Log**, and **task list** as continuity context for step-02 planning. If no `done` spec is found but an `in-review` spec exists for the same epic with a lower story number, HALT with status `blocked` and blocking condition `missing previous-story continuity decision`.
|
||||
|
||||
**B) Freeform path** — if the intent is not an epic story:
|
||||
- Planning artifacts are the output of BMAD phases 1-3. Typical files include:
|
||||
@@ -74,9 +75,9 @@ If the invocation prompt does not contain enough intent to identify what to impl
|
||||
4. Multi-goal warning. If the intent appears to contain multiple independently shippable goals, carry `multiple-goals` forward so step-02 can add it to `{spec_file}` frontmatter `warnings`. Do not split or block.
|
||||
5. Route:
|
||||
|
||||
**Folder+id dispatch:** derive a valid kebab-case slug from the entry's `title` (and `description` if needed) — the same kebab-casing convention as below, but never prefixed with `{story_id}`, since the id is already the filename's separate leading segment. Set `spec_file` = `{spec_folder}/stories/{story_id}-{slug}.md`. The id already disambiguates: no `{{.implementation_artifacts}}` fallback, no `-2`/`-3` suffixing.
|
||||
**Folder+id dispatch:** derive a valid kebab-case slug from the entry's `title` (and `description` if needed) — the same kebab-casing convention as below, but never prefixed with `{story_id}`, since the id is already the filename's separate leading segment. Set `spec_file` = `{spec_folder}/stories/{story_id}-{slug}.md`. The id already disambiguates: no `{implementation_artifacts}` fallback, no `-2`/`-3` suffixing.
|
||||
|
||||
**Otherwise:** derive a valid kebab-case slug from the clarified intent. If the intent references a tracking identifier (story number, issue number, ticket ID), lead the slug with it (e.g. `3-2-digest-delivery`, `gh-47-fix-auth`). If `{{.implementation_artifacts}}/spec-{slug}.md` already exists: if its status is `draft`, treat it as the same work and resume it (set `spec_file` to that path, **EARLY EXIT** → `./step-02-plan.md`); otherwise append `-2`, `-3`, etc. Set `spec_file` = `{{.implementation_artifacts}}/spec-{slug}.md`.
|
||||
**Otherwise:** derive a valid kebab-case slug from the clarified intent. If the intent references a tracking identifier (story number, issue number, ticket ID), lead the slug with it (e.g. `3-2-digest-delivery`, `gh-47-fix-auth`). If `{implementation_artifacts}/spec-{slug}.md` already exists: if its status is `draft`, treat it as the same work and resume it (set `spec_file` to that path, **EARLY EXIT** → `./step-02-plan.md`); otherwise append `-2`, `-3`, etc. Set `spec_file` = `{implementation_artifacts}/spec-{slug}.md`.
|
||||
|
||||
## NEXT
|
||||
|
||||
|
||||
@@ -1,22 +1,26 @@
|
||||
---
|
||||
deferred_work_file: '{implementation_artifacts}/deferred-work.md'
|
||||
---
|
||||
|
||||
# Step 2: Plan
|
||||
|
||||
## RULES
|
||||
|
||||
- **Language** — Speak in `{{.communication_language}}`. Write any file output in `{{.document_output_language}}`.
|
||||
- YOU MUST ALWAYS SPEAK OUTPUT in your Agent communication style with the config `{communication_language}`
|
||||
- No human interaction: do not ask questions or wait for approval in this step.
|
||||
|
||||
## INSTRUCTIONS
|
||||
|
||||
1. Draft resume check. If `{spec_file}` exists with `status: draft`, read it and capture the verbatim `<intent-contract>...</intent-contract>` block as `preserved_intent_contract`. Otherwise `preserved_intent_contract` is empty.
|
||||
2. Investigate codebase. _Read the code yourself for narrow, localized tasks. Isolate deep exploration in synchronous subagents: instruct them to give you distilled summaries only, and plan from those summaries._
|
||||
3. Read `./spec-template.md` fully. Fill it out based on the intent and investigation, resolving the template's `date` field to the current system date. If `{preserved_intent_contract}` is non-empty, substitute it for the `<intent-contract>` block in your filled spec before writing. Write the result to `{spec_file}`.
|
||||
3. Read `./spec-template.md` fully. Fill it out based on the intent and investigation. If `{preserved_intent_contract}` is non-empty, substitute it for the `<intent-contract>` block in your filled spec before writing. Write the result to `{spec_file}`.
|
||||
4. Self-review against READY FOR DEVELOPMENT standard.
|
||||
5. If intent gaps exist, do not fantasize and do not leave open questions. Multiple defensible readings of the intent that lead to observably different outcomes, with nothing in the intent to select between them, are an intent gap — do not resolve one by picking a reading. HALT with status `blocked`, blocking condition `intent gap`, and include the unanswered questions and evidence gathered.
|
||||
6. Warning check. If step-01 carried `multiple-goals`, add it to `{spec_file}` frontmatter `warnings`. If `{spec_file}` exceeds 1600 tokens, add `oversized` to frontmatter `warnings`. Continue either way.
|
||||
|
||||
### READY-FOR-DEVELOPMENT GATE
|
||||
|
||||
Re-read `./workflow.md`, then re-read `{spec_file}` from disk and verify the spec meets the READY FOR DEVELOPMENT standard.
|
||||
Re-read `./SKILL.md`, then re-read `{spec_file}` from disk and verify the spec meets the READY FOR DEVELOPMENT standard.
|
||||
|
||||
- **If the file is missing:** HALT with status `blocked` and blocking condition `planned spec file disappeared before implementation`.
|
||||
- **If the spec meets the standard:** set `{spec_file}` frontmatter status to `ready-for-dev`. If the invocation prompt directs a halt after planning (standard phrasing: `Halt after planning.` — accept any clear equivalent), HALT with status `ready-for-dev`; otherwise continue to step 3.
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
## RULES
|
||||
|
||||
- **Language** — Speak in `{{.communication_language}}`. Write any file output in `{{.document_output_language}}`.
|
||||
- YOU MUST ALWAYS SPEAK OUTPUT in your Agent communication style with the config `{communication_language}`
|
||||
- No human interaction: do not ask questions or wait for approval in this step.
|
||||
- Content inside `<intent-contract>` in `{spec_file}` is read-only. Do not modify.
|
||||
|
||||
@@ -23,11 +23,9 @@ Capture `baseline_revision` (current HEAD, or `NO_VCS` if version control is una
|
||||
|
||||
Change `{spec_file}` status to `in-progress` in the frontmatter before starting implementation.
|
||||
|
||||
Substitute the runtime placeholders (e.g. `{spec_file}`) into the implementation handoff below, then follow it verbatim. Do not add parent-authored goal restatements, file lists, ownership boundaries, or acceptance criteria to the handoff — the spec is the subagent's sole source of truth. If the handoff conflicts with the spec, HALT with status `blocked` and blocking condition `handoff conflicts with spec`, and include both conflicting passages.
|
||||
The implementation handoff is `{workflow.implementation_handoff}`, resolved during activation. Substitute the runtime placeholders (e.g. `{spec_file}`) into it, then follow it verbatim. Do not add parent-authored goal restatements, file lists, ownership boundaries, or acceptance criteria to the handoff — the spec is the subagent's sole source of truth. If the resolved handoff conflicts with the spec, HALT with status `blocked` and blocking condition `handoff conflicts with spec`, and include both conflicting passages.
|
||||
|
||||
{workflow.implementation_handoff}
|
||||
|
||||
Invoke the subagent **synchronously** and wait for it to return in this same turn — do not background/detach it (`run_in_background`) or end your turn to await a notification (see workflow.md → Subagents). Resume at "Verify" only after it returns. If the platform allows, keep the subagent available for re-engagement after it returns — step-04 may send it review fixes.
|
||||
Invoke the subagent **synchronously** and wait for it to return in this same turn — do not background/detach it (`run_in_background`) or end your turn to await a notification (see SKILL.md → Subagents). Resume at "Verify" only after it returns. If the platform allows, keep the subagent available for re-engagement after it returns — step-04 may send it review fixes.
|
||||
|
||||
**Path formatting rule:** Any markdown links written into `{spec_file}` must use paths relative to `{spec_file}`'s directory so they are clickable in VS Code. Any file paths displayed in terminal/conversation output must use CWD-relative format with `:line` notation (e.g., `src/path/file.ts:42`) for terminal clickability. No leading `/` in either case.
|
||||
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
---
|
||||
deferred_work_file: '{implementation_artifacts}/deferred-work.md'
|
||||
---
|
||||
|
||||
# Step 4: Review
|
||||
|
||||
## RULES
|
||||
|
||||
- **Language** — Speak in `{{.communication_language}}`. Write any file output in `{{.document_output_language}}`.
|
||||
- YOU MUST ALWAYS SPEAK OUTPUT in your Agent communication style with the config `{communication_language}`
|
||||
- No human interaction: do not ask questions or wait for approval in this step.
|
||||
- All review subagents must run at the same model capability as the current session.
|
||||
|
||||
@@ -18,11 +22,13 @@ Do NOT `git add` anything — this is read-only inspection.
|
||||
|
||||
### Review
|
||||
|
||||
The review layers are `{workflow.review_layers}`, resolved during activation.
|
||||
|
||||
Skip every layer whose `instruction` is empty or missing — that is how an override disables a default layer — and every layer whose `when` condition (if present) does not hold in the current context. If no layers remain, HALT with status `blocked` and blocking condition `no active review layers`.
|
||||
|
||||
Runtime placeholders: `{diff_output}` is the diff constructed above. `{verbatim_intent}` is the invocation intent exactly as this run received it at step-01; if the run started from an existing spec file rather than a fresh intent, it is the spec's `<intent-contract>` block instead.
|
||||
|
||||
Execute these review layers in parallel wherever their execution methods allow: substitute the runtime placeholders (e.g. `{diff_output}`) into each layer's instruction, then follow it verbatim. Parallel means several blocking calls awaited together in this turn — never backgrounded or detached, never ending the turn to await results (see workflow.md → Subagents). Spawn every reviewer subagent before reading or reacting to any of their output; begin collection and triage only once all are launched.
|
||||
|
||||
{workflow.review_layers}
|
||||
Execute all remaining layers in parallel wherever their execution methods allow: substitute the runtime placeholders (e.g. `{diff_output}`) into each layer's `instruction`, then follow it verbatim. Parallel means several blocking calls awaited together in this turn — never backgrounded or detached, never ending the turn to await results (see SKILL.md → Subagents). Spawn every reviewer subagent before reading or reacting to any of their output; begin collection and triage only once all are launched.
|
||||
|
||||
### Classify
|
||||
|
||||
@@ -50,17 +56,17 @@ Execute these review layers in parallel wherever their execution methods allow:
|
||||
- addressed_findings:
|
||||
- `[high|medium|low]` `[patch|bad_spec]` <finding summary and action taken in this pass>
|
||||
```
|
||||
Where `{date}` is the current system date and `count` is either just `0`, or total with breakdown by severity `N: (high Nhigh, medium Nmedium, low Nlow)`.
|
||||
Where `count` is either just `0`, or total with breakdown by severity `N: (high Nhigh, medium Nmedium, low Nlow)`.
|
||||
If no patch was fixed and no bad_spec repair loopback was triggered in this pass, write:
|
||||
```markdown
|
||||
- addressed_findings:
|
||||
- none
|
||||
```
|
||||
5. Process findings in cascading order. If intent_gap exists, lower findings are moot; follow the intent_gap branch below. If bad_spec exists, lower findings are moot since code will be re-derived. If neither exists, process patch and defer normally. Before each bad_spec loopback, read `{spec_file}` frontmatter `review_loop_iteration` (missing means `0`), increment it by 1, and write it back. If it exceeds 5, append the triage-log entry for this pass with `addressed_findings: none`, then HALT with status `blocked` and blocking condition `review repair loop exceeded 5 iterations (non-convergence)`.
|
||||
- **intent_gap** — Root cause is inside `<intent-contract>`. Save the attempted change as a patch file in `{{.implementation_artifacts}}` and reference it from the triage-log entry, then revert code changes. Append the triage-log entry for this pass with `addressed_findings: none`, then HALT with status `blocked`, blocking condition `intent gap`, and include the unresolved questions and the saved patch path.
|
||||
- **intent_gap** — Root cause is inside `<intent-contract>`. Save the attempted change as a patch file in `{implementation_artifacts}` and reference it from the triage-log entry, then revert code changes. Append the triage-log entry for this pass with `addressed_findings: none`, then HALT with status `blocked`, blocking condition `intent gap`, and include the unresolved questions and the saved patch path.
|
||||
- **bad_spec** — Root cause is outside `<intent-contract>`. Do not modify content inside `<intent-contract>`. Before reverting code: extract KEEP instructions for positive preservation (what worked well and must survive re-derivation). Revert code changes. Read the `## Spec Change Log` in `{spec_file}` and strictly respect all logged constraints when amending the sections outside `<intent-contract>` that contain the root cause. Append a new change-log entry recording: the triggering finding, what was amended, the known-bad state avoided, and the KEEP instructions. Append the triage-log entry for this pass, listing every bad_spec finding that triggered the spec amendment and implementation loopback under `addressed_findings`. Read fully and follow `./step-03-implement.md` to re-derive the code, then this step will run again.
|
||||
- **patch** — Auto-fix. These are the only findings that survive loopbacks. If the step-03 implementation subagent can be re-engaged with its context intact, send it all patch findings in one synchronous message — for each: the file, what is wrong, and what the fix must do. If it cannot be re-engaged, apply the patches yourself. Then re-run the commands in `{spec_file}`'s `## Verification` section (or perform its manual checks); if verification fails and the failure cannot be fixed, HALT with status `blocked` and blocking condition `patch verification failed`. Append the triage-log entry for this pass, listing every patch fixed in this pass under `addressed_findings`.
|
||||
- **defer** — Append one new entry to `{{.deferred_work_file}}` using this format. Do not modify existing entries or look for duplicates.
|
||||
- **defer** — Append one new entry to `{deferred_work_file}` using this format. Do not modify existing entries or look for duplicates.
|
||||
```markdown
|
||||
- source_spec: `{spec_file}`
|
||||
summary: <one sentence>
|
||||
|
||||
@@ -1,104 +0,0 @@
|
||||
# Dev Auto Workflow
|
||||
|
||||
**Goal:** Turn intent into a hardened, reviewable artifact, without human interaction.
|
||||
|
||||
**CRITICAL:** If a step says "read fully and follow step-XX", you read and follow step-XX. No exceptions.
|
||||
|
||||
## HALT
|
||||
|
||||
To HALT with a final status and optional blocking condition:
|
||||
|
||||
1. **Folder+id dispatch** (`{spec_folder}` and `{story_id}` are set): the write-back always lands at the id-keyed story spec. The `{{.implementation_artifacts}}` fallback in step 2 below is never used in this mode, even for halts before planning starts.
|
||||
- If `{spec_file}` is still empty, resolve it now:
|
||||
- **Entry not resolved** (`stories.yaml` is missing/unparseable, or `{story_id}` has no matching entry): use the fixed slug segment `unresolved`: `{spec_file}` = `{spec_folder}/stories/{story_id}-unresolved.md`.
|
||||
- **Ambiguous on-disk match** (the halt is `ambiguous story file match` — more than one file already matches `{spec_folder}/stories/{story_id}-*.md`): use the fixed slug segment `ambiguous` instead of deriving from the title, so the write-back neither creates a third title-derived candidate nor risks silently landing on one of the existing ambiguous files: `{spec_file}` = `{spec_folder}/stories/{story_id}-ambiguous.md`.
|
||||
- **Otherwise** (the entry was resolved and no ambiguous on-disk match exists): derive `{spec_file}` = `{spec_folder}/stories/{story_id}-{slug}.md`, where `{slug}` is a kebab-case slug from `title` (and `description` if needed) with no `{story_id}` prefix — the same derivation step-01's Route uses.
|
||||
- If `{spec_file}` exists on disk, update `status` in frontmatter and append missing result details under `## Auto Run Result`.
|
||||
- If it does not exist, create it as a skeletal story spec:
|
||||
```markdown
|
||||
---
|
||||
status: <final status>
|
||||
---
|
||||
|
||||
# <entry title, or "Story {story_id}" if the entry could not be resolved or the on-disk match was ambiguous>
|
||||
|
||||
## Auto Run Result
|
||||
|
||||
Status: <final status>
|
||||
Blocking condition: <blocking condition, if any>
|
||||
```
|
||||
2. **Otherwise:**
|
||||
- If `{spec_file}` is known and exists, update `status` in frontmatter and append missing result details under `## Auto Run Result`.
|
||||
- If `{spec_file}` is unknown or missing, create `{{.implementation_artifacts}}/bmad-dev-auto-result-<slug-or-timestamp>.md` with:
|
||||
```markdown
|
||||
---
|
||||
status: <final status>
|
||||
---
|
||||
|
||||
# BMad Dev Auto Result
|
||||
|
||||
Status: <final status>
|
||||
Blocking condition: <blocking condition, if any>
|
||||
```
|
||||
3. Follow **On Complete** below, then stop the workflow.
|
||||
|
||||
### On Complete
|
||||
|
||||
If anything appears below, follow it as the final terminal instruction before exiting; otherwise exit normally.
|
||||
|
||||
{workflow.on_complete}
|
||||
|
||||
## Subagents
|
||||
|
||||
Using subagents when instructed is mandatory. If you cannot, HALT with status `blocked` and blocking condition `no subagents`.
|
||||
|
||||
Invoke every subagent **synchronously**: launch it, wait for it to return within the same turn, then continue with its result. When a step says to run subagents "in parallel" (e.g. the reviewers), that means several **blocking** calls awaited together in one turn — not detached execution. Never run a subagent in the background / detached / async (e.g. `run_in_background: true`), and never end your turn to "await a completion notification." This workflow runs unattended: there is no event loop to resume a yielded turn, so a backgrounded subagent never hands control back and the run stalls. The only sanctioned way to end a turn is the HALT protocol above with an explicit terminal `status`.
|
||||
|
||||
## READY FOR DEVELOPMENT STANDARD
|
||||
|
||||
A specification is "Ready for Development" when:
|
||||
|
||||
- **Actionable**: Every task has a file path and specific action.
|
||||
- **Logical**: Tasks ordered by dependency.
|
||||
- **Testable**: All ACs use Given/When/Then.
|
||||
- **Surface-anchored**: ACs observe the outermost surface the intent references — never a more internal proxy for it (e.g. the API response, not the database row behind it).
|
||||
- **Complete**: No placeholders or TBDs.
|
||||
- **Sufficient**: No known requirement, acceptance, dependency, or implementation gaps remain unresolved.
|
||||
- **Coherent**: No unresolved ambiguities or internal contradictions.
|
||||
|
||||
## Conventions
|
||||
|
||||
- Bare paths (e.g. `step-01-clarify-and-route.md`) resolve from the skill root.
|
||||
- `{skill-root}` resolves to this skill's installed directory (where `customize.toml` lives).
|
||||
- `{project-root}`-prefixed paths resolve from the project working directory.
|
||||
- `{skill-name}` resolves to the skill directory's basename.
|
||||
|
||||
## On Activation
|
||||
|
||||
### Step 1: Execute Prepend Steps
|
||||
|
||||
Execute each of these steps in order before proceeding (`_None._` means skip):
|
||||
|
||||
{workflow.activation_steps_prepend}
|
||||
|
||||
### Step 2: Load Persistent Facts
|
||||
|
||||
Treat every entry below as foundational context you carry for the rest of the workflow run. Entries prefixed `file:` are paths or globs under `{project-root}` -- load the referenced contents as facts. All other entries are facts verbatim (`_None._` means none):
|
||||
|
||||
{workflow.persistent_facts}
|
||||
|
||||
### Step 3: Execute Append Steps
|
||||
|
||||
Execute each of these steps in order (`_None._` means skip):
|
||||
|
||||
{workflow.activation_steps_append}
|
||||
|
||||
Activation is complete after all activation steps have run.
|
||||
|
||||
## Workflow Execution
|
||||
|
||||
Follow the step files in order. Read one step fully, execute it, then load the next step only when directed. Do not skip, reorder, or pre-load steps.
|
||||
|
||||
## First workflow step
|
||||
|
||||
Read fully and follow: `./step-01-clarify-and-route.md` to begin the workflow.
|
||||
@@ -1,413 +0,0 @@
|
||||
/**
|
||||
* Smoke test for bmad-dev-auto render.py
|
||||
*
|
||||
* Sets up a temp project with base + override config layers and a
|
||||
* _bmad/custom/bmad-dev-auto.user.toml [workflow] override, runs render.py,
|
||||
* and asserts:
|
||||
* 1. render.py stays in sync with bmad-quick-dev's (skill-name refs aside) —
|
||||
* the renderer is deliberately duplicated per skill, so drift is a bug.
|
||||
* 2. The central-config override wins (step files' language line contains
|
||||
* "Japanese") and paths bake absolute into the rendered output.
|
||||
* 3. [workflow] customization is self-resolved and inlined: prepend bullet,
|
||||
* persistent_facts append (base kept), empty list -> _None._, on_complete
|
||||
* scalar baked into workflow.md, implementation_handoff baked into
|
||||
* step-03 with runtime {spec_file} surviving.
|
||||
* 4. Review layers materialize as direct invocation blocks in step-04: an
|
||||
* override replacing a layer by id wins, an empty-instruction override
|
||||
* drops its layer, a `when` renders as a run-time guard, runtime
|
||||
* placeholders like {diff_output}/{verbatim_intent} survive, and
|
||||
* disabling every layer renders the HALT instruction.
|
||||
* 5. No {workflow.*} placeholder or resolve_customization.py call survives
|
||||
* in any rendered file.
|
||||
*
|
||||
* Usage: node test/test-dev-auto-renderer.js
|
||||
* Exit codes: 0 = all tests pass, 1 = test failures
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const fs = require('node:fs');
|
||||
const os = require('node:os');
|
||||
const path = require('node:path');
|
||||
const { spawnSync } = require('node:child_process');
|
||||
|
||||
// ANSI color codes (same as other test files)
|
||||
const colors = {
|
||||
reset: '\u001B[0m',
|
||||
green: '\u001B[32m',
|
||||
red: '\u001B[31m',
|
||||
cyan: '\u001B[36m',
|
||||
};
|
||||
|
||||
let totalTests = 0;
|
||||
let passedTests = 0;
|
||||
const failures = [];
|
||||
|
||||
function test(name, fn) {
|
||||
totalTests++;
|
||||
try {
|
||||
fn();
|
||||
passedTests++;
|
||||
console.log(` ${colors.green}\u2713${colors.reset} ${name}`);
|
||||
} catch (error) {
|
||||
console.log(` ${colors.red}\u2717${colors.reset} ${name} ${colors.red}${error.message}${colors.reset}`);
|
||||
failures.push({ name, message: error.message });
|
||||
}
|
||||
}
|
||||
|
||||
function assert(condition, message) {
|
||||
if (!condition) throw new Error(message);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const SKILL_SRC = path.join(__dirname, '..', 'src', 'bmm-skills', '4-implementation', 'bmad-dev-auto');
|
||||
const QUICK_DEV_SRC = path.join(__dirname, '..', 'src', 'bmm-skills', '4-implementation', 'bmad-quick-dev');
|
||||
|
||||
/**
|
||||
* Recursively copy a directory (stdlib only, no fs.cp to stay >=20 compat).
|
||||
*/
|
||||
function copyDirSync(src, dst) {
|
||||
fs.mkdirSync(dst, { recursive: true });
|
||||
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
|
||||
const srcPath = path.join(src, entry.name);
|
||||
const dstPath = path.join(dst, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
copyDirSync(srcPath, dstPath);
|
||||
} else {
|
||||
fs.copyFileSync(srcPath, dstPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Extra one-off temp projects created by makeProject(); cleaned up in finally.
|
||||
const extraTmpDirs = [];
|
||||
|
||||
/**
|
||||
* Spin up an isolated temp project with the given _bmad/config.toml body and a
|
||||
* copy of the skill dir, so a single bad-config scenario can be rendered in
|
||||
* isolation. Returns { dir, skillDst }; the caller runs render.py against it.
|
||||
*/
|
||||
function makeProject(configText) {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'bmad-dev-auto-renderer-halt-'));
|
||||
extraTmpDirs.push(dir);
|
||||
fs.mkdirSync(path.join(dir, '_bmad'), { recursive: true });
|
||||
fs.writeFileSync(path.join(dir, '_bmad', 'config.toml'), configText, 'utf-8');
|
||||
const skillDst = path.join(dir, 'bmad-dev-auto');
|
||||
copyDirSync(SKILL_SRC, skillDst);
|
||||
return { dir, skillDst };
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Test fixture setup
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'bmad-dev-auto-renderer-test-'));
|
||||
|
||||
try {
|
||||
console.log(`\n${colors.cyan}Dev-auto renderer smoke tests${colors.reset}\n`);
|
||||
|
||||
test('render.py stays in sync with the quick-dev renderer (skill-name refs aside)', () => {
|
||||
const normalize = (source) =>
|
||||
source
|
||||
.replaceAll('bmad-quick-dev', '<skill>')
|
||||
.replaceAll('bmad-dev-auto', '<skill>')
|
||||
.replaceAll('quick-dev only reads', '<skill> only reads')
|
||||
.replaceAll('dev-auto only reads', '<skill> only reads');
|
||||
const devAuto = fs.readFileSync(path.join(SKILL_SRC, 'render.py'), 'utf-8');
|
||||
const quickDev = fs.readFileSync(path.join(QUICK_DEV_SRC, 'render.py'), 'utf-8');
|
||||
assert(
|
||||
normalize(devAuto) === normalize(quickDev),
|
||||
'bmad-dev-auto/render.py has drifted from bmad-quick-dev/render.py beyond skill-name references — propagate the change to both copies',
|
||||
);
|
||||
});
|
||||
|
||||
// _bmad/config.toml — base layer
|
||||
fs.mkdirSync(path.join(tmpDir, '_bmad'), { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(tmpDir, '_bmad', 'config.toml'),
|
||||
[
|
||||
'[core]',
|
||||
'communication_language = "French"',
|
||||
'document_output_language = "Klingon"',
|
||||
'',
|
||||
'[modules.bmm]',
|
||||
'planning_artifacts = "{project-root}/plan"',
|
||||
'implementation_artifacts = "{project-root}/impl"',
|
||||
].join('\n'),
|
||||
'utf-8',
|
||||
);
|
||||
|
||||
// _bmad/custom/config.user.toml — override layer (should win)
|
||||
fs.mkdirSync(path.join(tmpDir, '_bmad', 'custom'), { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(tmpDir, '_bmad', 'custom', 'config.user.toml'),
|
||||
['[core]', 'communication_language = "Japanese"'].join('\n'),
|
||||
'utf-8',
|
||||
);
|
||||
|
||||
// _bmad/custom/bmad-dev-auto.user.toml — [workflow] customization override.
|
||||
// Exercises render.py's self-resolution: array append (persistent_facts),
|
||||
// list inlining (activation_steps_prepend), scalar overrides (on_complete,
|
||||
// implementation_handoff), and review-layer keyed merge, all baked into the
|
||||
// rendered output with no runtime resolve_customization.py.
|
||||
fs.writeFileSync(
|
||||
path.join(tmpDir, '_bmad', 'custom', 'bmad-dev-auto.user.toml'),
|
||||
[
|
||||
'[workflow]',
|
||||
'activation_steps_prepend = ["TEST_PREPEND_STEP"]',
|
||||
'persistent_facts = ["TEST_EXTRA_FACT"]',
|
||||
'on_complete = "TEST_ON_COMPLETE_INSTRUCTION"',
|
||||
'implementation_handoff = "TEST_HANDOFF_INSTRUCTION for {spec_file}"',
|
||||
'',
|
||||
'[[workflow.review_layers]]',
|
||||
'id = "edge-case-hunter"',
|
||||
'name = "Replaced Layer"',
|
||||
'when = "TEST_WHEN_CONDITION"',
|
||||
'instruction = "TEST_REPLACED_LAYER_INSTRUCTION"',
|
||||
'',
|
||||
'[[workflow.review_layers]]',
|
||||
'id = "verification-gap"',
|
||||
'instruction = ""',
|
||||
].join('\n'),
|
||||
'utf-8',
|
||||
);
|
||||
|
||||
// Copy skill dir into <tmpDir>/bmad-dev-auto/ so find_project_root() walks
|
||||
// up and finds <tmpDir>/_bmad/, and os.path.basename(script_dir) resolves
|
||||
// to the real skill name so the render output lands at
|
||||
// _bmad/render/bmad-dev-auto/workflow.md.
|
||||
const skillDst = path.join(tmpDir, 'bmad-dev-auto');
|
||||
copyDirSync(SKILL_SRC, skillDst);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Run render.py
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const result = spawnSync('python3', [path.join(skillDst, 'render.py')], {
|
||||
cwd: skillDst,
|
||||
encoding: 'utf-8',
|
||||
});
|
||||
|
||||
const renderDir = path.join(tmpDir, '_bmad', 'render', 'bmad-dev-auto');
|
||||
const readRendered = (name) => fs.readFileSync(path.join(renderDir, name), 'utf-8');
|
||||
const renderedMdFiles = () => fs.readdirSync(renderDir).filter((f) => f.endsWith('.md'));
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Tests
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
test('render.py exits with code 0', () => {
|
||||
assert(result.status === 0, `exit code ${result.status}\nstdout: ${result.stdout}\nstderr: ${result.stderr}`);
|
||||
});
|
||||
|
||||
test('workflow.md exists in render output and stdout dispatches to it', () => {
|
||||
const rendered = path.join(renderDir, 'workflow.md');
|
||||
assert(fs.existsSync(rendered), `workflow.md not found at ${rendered}`);
|
||||
assert(result.stdout.includes('read and follow'), `stdout missing the dispatch line.\nstdout: ${result.stdout}`);
|
||||
assert(result.stdout.includes('workflow.md'), `stdout dispatch does not point at workflow.md.\nstdout: ${result.stdout}`);
|
||||
});
|
||||
|
||||
test('custom override wins — communication_language baked into step files', () => {
|
||||
const content = readRendered('step-01-clarify-and-route.md');
|
||||
assert(content.includes('Japanese'), 'communication_language override (Japanese) did not win in the step-01 language line');
|
||||
});
|
||||
|
||||
test('document_output_language bakes into the per-step language line', () => {
|
||||
const content = readRendered('step-01-clarify-and-route.md');
|
||||
assert(content.includes('Klingon'), 'document_output_language not baked into the step-01 language line');
|
||||
});
|
||||
|
||||
test('deferred_work_file is an absolute path rooted at temp project dir', () => {
|
||||
const content = readRendered('step-04-review.md');
|
||||
// Normalize to forward slashes for cross-platform matching
|
||||
const normalizedTmp = tmpDir.replaceAll('\\', '/');
|
||||
const expected = `${normalizedTmp}/impl/deferred-work.md`;
|
||||
assert(
|
||||
content.includes(expected),
|
||||
`deferred_work_file path not found.\nExpected substring: ${expected}\n` +
|
||||
`step-04-review.md excerpt (first 2000 chars):\n${content.slice(0, 2000)}`,
|
||||
);
|
||||
});
|
||||
|
||||
test('workflow override — prepend step inlined as a bullet', () => {
|
||||
const content = readRendered('workflow.md');
|
||||
assert(content.includes('- TEST_PREPEND_STEP'), 'activation_steps_prepend not inlined as a bullet');
|
||||
});
|
||||
|
||||
test('workflow override — persistent_facts append (base kept, override added)', () => {
|
||||
const content = readRendered('workflow.md');
|
||||
assert(content.includes('- TEST_EXTRA_FACT'), 'override persistent_fact not inlined');
|
||||
assert(content.includes('project-context.md'), 'base persistent_fact dropped — append semantics broken');
|
||||
});
|
||||
|
||||
test('empty activation_steps_append renders the _None._ sentinel', () => {
|
||||
const content = readRendered('workflow.md');
|
||||
assert(content.includes('_None._'), '_None._ sentinel missing for empty list');
|
||||
});
|
||||
|
||||
test('on_complete scalar inlined into the HALT On Complete section', () => {
|
||||
assert(readRendered('workflow.md').includes('TEST_ON_COMPLETE_INSTRUCTION'), 'on_complete not inlined into workflow.md');
|
||||
});
|
||||
|
||||
test('implementation_handoff inlined into step-03 with runtime {spec_file} surviving', () => {
|
||||
const content = readRendered('step-03-implement.md');
|
||||
assert(content.includes('TEST_HANDOFF_INSTRUCTION'), 'implementation_handoff override not inlined into step-03');
|
||||
assert(content.includes('{spec_file}'), 'runtime {spec_file} placeholder did not survive rendering');
|
||||
});
|
||||
|
||||
test('review layers materialize as invocation blocks in step-04', () => {
|
||||
const content = readRendered('step-04-review.md');
|
||||
assert(content.includes('#### Blind Hunter'), 'default review layer not rendered as a #### invocation block');
|
||||
assert(content.includes('#### Intent Alignment Auditor'), 'intent-alignment layer not rendered as a #### invocation block');
|
||||
assert(!content.includes('- id:'), 'layer table data leaked into the rendered output');
|
||||
assert(content.includes('{diff_output}'), 'runtime {diff_output} placeholder did not survive rendering');
|
||||
assert(content.includes('{verbatim_intent}'), 'runtime {verbatim_intent} placeholder did not survive rendering');
|
||||
});
|
||||
|
||||
test('review layer override replaces the matching default by id', () => {
|
||||
const content = readRendered('step-04-review.md');
|
||||
assert(content.includes('#### Replaced Layer'), 'override layer name not used as block title');
|
||||
assert(content.includes('TEST_REPLACED_LAYER_INSTRUCTION'), 'override layer instruction not inlined');
|
||||
assert(!content.includes('bmad-review-edge-case-hunter'), 'replaced default layer instruction still present');
|
||||
assert(content.includes('bmad-review-adversarial-general'), 'untouched default layer dropped by keyed merge');
|
||||
});
|
||||
|
||||
test('empty-instruction override drops its layer entirely', () => {
|
||||
const content = readRendered('step-04-review.md');
|
||||
assert(!content.includes('verification-gap'), 'disabled layer id still present in rendered output');
|
||||
assert(!content.includes('Verification Gap Reviewer'), 'disabled layer name still present in rendered output');
|
||||
});
|
||||
|
||||
test('when condition renders as a run-time guard line', () => {
|
||||
const content = readRendered('step-04-review.md');
|
||||
assert(
|
||||
content.includes('Run this layer only if the following holds in the current context: `TEST_WHEN_CONDITION`'),
|
||||
'when condition not rendered as a guard line',
|
||||
);
|
||||
});
|
||||
|
||||
test('disabling every layer renders the HALT instruction', () => {
|
||||
// Second render pass: replace the override file so every default layer is
|
||||
// disabled, then re-render.
|
||||
fs.writeFileSync(
|
||||
path.join(tmpDir, '_bmad', 'custom', 'bmad-dev-auto.user.toml'),
|
||||
[
|
||||
'[workflow]',
|
||||
'',
|
||||
'[[workflow.review_layers]]',
|
||||
'id = "blind-hunter"',
|
||||
'instruction = ""',
|
||||
'',
|
||||
'[[workflow.review_layers]]',
|
||||
'id = "edge-case-hunter"',
|
||||
'instruction = ""',
|
||||
'',
|
||||
'[[workflow.review_layers]]',
|
||||
'id = "verification-gap"',
|
||||
'instruction = ""',
|
||||
'',
|
||||
'[[workflow.review_layers]]',
|
||||
'id = "intent-alignment"',
|
||||
'instruction = ""',
|
||||
].join('\n'),
|
||||
'utf-8',
|
||||
);
|
||||
const rerun = spawnSync('python3', [path.join(skillDst, 'render.py')], {
|
||||
cwd: skillDst,
|
||||
encoding: 'utf-8',
|
||||
});
|
||||
assert(rerun.status === 0, `re-render exit code ${rerun.status}\nstderr: ${rerun.stderr}`);
|
||||
const halt = 'No review layers are active. HALT with status `blocked` and blocking condition `no active review layers`.';
|
||||
assert(readRendered('step-04-review.md').includes(halt), 'HALT instruction missing from step-04-review.md');
|
||||
});
|
||||
|
||||
test('no {workflow.*} placeholder survives in any rendered file', () => {
|
||||
const leaks = renderedMdFiles().filter((f) => readRendered(f).includes('{workflow.'));
|
||||
assert(leaks.length === 0, `{workflow.*} leaked in: ${leaks.join(', ')}`);
|
||||
});
|
||||
|
||||
test('no resolve_customization.py reference survives in any rendered file', () => {
|
||||
const leaks = renderedMdFiles().filter((f) => readRendered(f).includes('resolve_customization.py'));
|
||||
assert(leaks.length === 0, `resolve_customization.py still referenced in: ${leaks.join(', ')}`);
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Bad-config HALTs cleanly (never a raw Python traceback)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
test('missing planning_artifacts HALTs cleanly (no traceback)', () => {
|
||||
// implementation_artifacts is present, so this exercises the general
|
||||
// missing-vars scan rather than the dedicated guard.
|
||||
const { skillDst: dst } = makeProject(
|
||||
[
|
||||
'[core]',
|
||||
'communication_language = "French"',
|
||||
'document_output_language = "Klingon"',
|
||||
'implementation_artifacts = "{project-root}/impl"',
|
||||
].join('\n'),
|
||||
);
|
||||
const res = spawnSync('python3', [path.join(dst, 'render.py')], { cwd: dst, encoding: 'utf-8' });
|
||||
assert(res.status === 1, `expected exit 1, got ${res.status}\nstdout: ${res.stdout}\nstderr: ${res.stderr}`);
|
||||
assert(
|
||||
res.stdout.includes('HALT and report to the user: config is missing') && res.stdout.includes('`planning_artifacts`'),
|
||||
`stdout missing the planning_artifacts HALT directive.\nstdout: ${res.stdout}`,
|
||||
);
|
||||
assert(
|
||||
res.stdout.includes('step-01-clarify-and-route.md'),
|
||||
`HALT directive does not name the referencing file.\nstdout: ${res.stdout}`,
|
||||
);
|
||||
assert(!res.stderr.includes('Traceback'), `renderer crashed with a traceback instead of HALTing:\n${res.stderr}`);
|
||||
});
|
||||
|
||||
test('unparseable customization override HALTs cleanly (no traceback)', () => {
|
||||
const { dir, skillDst: dst } = makeProject(
|
||||
[
|
||||
'[core]',
|
||||
'communication_language = "French"',
|
||||
'document_output_language = "Klingon"',
|
||||
'planning_artifacts = "{project-root}/plan"',
|
||||
'implementation_artifacts = "{project-root}/impl"',
|
||||
].join('\n'),
|
||||
);
|
||||
fs.mkdirSync(path.join(dir, '_bmad', 'custom'), { recursive: true });
|
||||
fs.writeFileSync(path.join(dir, '_bmad', 'custom', 'bmad-dev-auto.user.toml'), '[workflow\non_complete = broken', 'utf-8');
|
||||
const res = spawnSync('python3', [path.join(dst, 'render.py')], { cwd: dst, encoding: 'utf-8' });
|
||||
assert(res.status === 1, `expected exit 1, got ${res.status}\nstdout: ${res.stdout}\nstderr: ${res.stderr}`);
|
||||
assert(
|
||||
res.stdout.includes('HALT and report to the user: failed to parse') && res.stdout.includes('bmad-dev-auto.user.toml'),
|
||||
`stdout missing the failed-to-parse HALT directive naming the override file.\nstdout: ${res.stdout}`,
|
||||
);
|
||||
assert(!res.stderr.includes('Traceback'), `renderer crashed with a traceback instead of HALTing:\n${res.stderr}`);
|
||||
});
|
||||
} finally {
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||
for (const dir of extraTmpDirs) {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Summary
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
console.log(`\n${colors.cyan}${'═'.repeat(55)}${colors.reset}`);
|
||||
console.log(`${colors.cyan}Test Results:${colors.reset}`);
|
||||
console.log(` Total: ${totalTests}`);
|
||||
console.log(` Passed: ${colors.green}${passedTests}${colors.reset}`);
|
||||
console.log(` Failed: ${passedTests === totalTests ? colors.green : colors.red}${totalTests - passedTests}${colors.reset}`);
|
||||
console.log(`${colors.cyan}${'═'.repeat(55)}${colors.reset}\n`);
|
||||
|
||||
if (failures.length > 0) {
|
||||
console.log(`${colors.red}FAILED TESTS:${colors.reset}\n`);
|
||||
for (const failure of failures) {
|
||||
console.log(`${colors.red}\u2717${colors.reset} ${failure.name}`);
|
||||
console.log(` ${failure.message}\n`);
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(`${colors.green}All tests passed!${colors.reset}\n`);
|
||||
process.exit(0);
|
||||
@@ -80,7 +80,7 @@ function escapeTableCell(str) {
|
||||
}
|
||||
|
||||
// Path prefixes/patterns that only exist in installed structure, not in source
|
||||
const INSTALL_ONLY_PATHS = ['_config/', 'custom/', 'render/bmad-quick-dev/', 'render/bmad-dev-auto/'];
|
||||
const INSTALL_ONLY_PATHS = ['_config/', 'custom/', 'render/bmad-quick-dev/'];
|
||||
|
||||
// Files that are generated at install time and don't exist in the source tree
|
||||
const INSTALL_GENERATED_FILES = ['config.yaml', 'config.user.yaml'];
|
||||
|
||||
Reference in New Issue
Block a user