bmad-method 6.10.1-next.37 → 6.10.1-next.39

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.
Files changed (23) hide show
  1. package/package.json +4 -5
  2. package/src/bmm-skills/ship/bmad-build/SKILL.md +4 -4
  3. package/src/bmm-skills/ship/bmad-build/review-prompts/verification-gap.md +4 -2
  4. package/src/bmm-skills/ship/bmad-build/step-01-clarify-and-route.md +12 -12
  5. package/src/bmm-skills/ship/bmad-build/step-02-plan.md +3 -3
  6. package/src/bmm-skills/ship/bmad-build/step-03-implement.md +2 -2
  7. package/src/bmm-skills/ship/bmad-build/step-04-review.md +4 -4
  8. package/src/bmm-skills/ship/bmad-build/step-05-present.md +1 -1
  9. package/src/bmm-skills/ship/bmad-build/step-oneshot.md +5 -5
  10. package/src/bmm-skills/ship/bmad-build/sync-sprint-status.md +2 -2
  11. package/src/bmm-skills/ship/bmad-build/workflow.md +3 -5
  12. package/src/bmm-skills/ship/bmad-build-auto/SKILL.md +1 -1
  13. package/src/bmm-skills/ship/bmad-build-auto/review-prompts/verification-gap.md +4 -2
  14. package/src/bmm-skills/ship/bmad-code-review/review-prompts/verification-gap.md +4 -2
  15. package/src/bmm-skills/ship/bmad-retrospective/scripts/__pycache__/sprint_status.cpython-311.pyc +0 -0
  16. package/src/bmm-skills/ship/bmad-retrospective/scripts/tests/__pycache__/test_git_evidence.cpython-311-pytest-9.1.1.pyc +0 -0
  17. package/src/bmm-skills/ship/bmad-retrospective/scripts/tests/__pycache__/test_sprint_status.cpython-311-pytest-9.1.1.pyc +0 -0
  18. package/src/scripts/__pycache__/config_utils.cpython-311.pyc +0 -0
  19. package/src/scripts/render_skill.py +2 -1
  20. package/src/scripts/tests/__pycache__/test_config_utils.cpython-311.pyc +0 -0
  21. package/src/scripts/tests/__pycache__/test_resolve_config.cpython-311.pyc +0 -0
  22. package/src/scripts/tests/__pycache__/test_resolve_customization.cpython-311.pyc +0 -0
  23. package/src/bmm-skills/ship/bmad-build/render.py +0 -420
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package.json",
3
3
  "name": "bmad-method",
4
- "version": "6.10.1-next.37",
4
+ "version": "6.10.1-next.39",
5
5
  "description": "Breakthrough Method of Agile AI-driven Development",
6
6
  "keywords": [
7
7
  "agile",
@@ -40,14 +40,13 @@
40
40
  "lint:fix": "eslint . --ext .js,.cjs,.mjs,.yaml --fix",
41
41
  "lint:md": "markdownlint-cli2 \"**/*.md\"",
42
42
  "prepare": "command -v husky >/dev/null 2>&1 && husky || exit 0",
43
- "quality": "npm run format:check && npm run lint && npm run lint:md && npm run docs:build && npm run test:site-url && npm run test:install && npm run test:urls && npm run test:renderer && npm run test:retrospective && npm run test:build-auto-contract && npm run validate:refs && npm run validate:skills && npm run docs:validate-sidebar",
43
+ "quality": "npm run format:check && npm run lint && npm run lint:md && npm run docs:build && npm run test:site-url && npm run test:install && npm run test:urls && npm run test:renderer && npm run test:retrospective && npm run validate:refs && npm run validate:skills && npm run docs:validate-sidebar",
44
44
  "rebundle": "node tools/installer/bundlers/bundle-web.js rebundle",
45
- "test": "npm run test:refs && npm run test:install && npm run test:urls && npm run test:site-url && npm run test:channels && npm run test:renderer && npm run test:retrospective && npm run test:build-auto-contract && npm run test:skills && npm run lint && npm run lint:md && npm run format:check",
46
- "test:build-auto-contract": "node test/test-build-auto-contract.js",
45
+ "test": "npm run test:refs && npm run test:install && npm run test:urls && npm run test:site-url && npm run test:channels && npm run test:renderer && npm run test:retrospective && npm run test:skills && npm run lint && npm run lint:md && npm run format:check",
47
46
  "test:channels": "node test/test-installer-channels.js",
48
47
  "test:install": "node test/test-installation-components.js",
49
48
  "test:refs": "node test/test-file-refs-csv.js",
50
- "test:renderer": "uv run --python 3.11 python -m unittest src/scripts/tests/test_config_utils.py src/scripts/tests/test_resolve_config.py src/scripts/tests/test_resolve_customization.py && node test/test-build-renderer.js && node test/test-build-auto-renderer.js",
49
+ "test:renderer": "uv run --python 3.11 python -m unittest src/scripts/tests/test_config_utils.py src/scripts/tests/test_resolve_config.py src/scripts/tests/test_resolve_customization.py && node test/test-build-auto-renderer.js",
51
50
  "test:retrospective": "uv run --python 3.11 src/bmm-skills/ship/bmad-retrospective/scripts/tests/test_git_evidence.py && uv run --python 3.11 src/bmm-skills/ship/bmad-retrospective/scripts/tests/test_sprint_status.py",
52
51
  "test:site-url": "node test/test-site-url.mjs",
53
52
  "test:skills": "node test/test-validate-skills.js",
@@ -3,11 +3,11 @@ name: bmad-build
3
3
  description: 'Implements any user intent, requirement, story, bug fix or change request by producing clean working code artifacts that follow the project''s existing architecture, patterns and conventions. Use when the user wants to build, fix, tweak, refactor, add or modify any code, component or feature.'
4
4
  ---
5
5
 
6
- Run this, substituting `{skill-root}` with the absolute path to this skill's base directory, without changing the cwd:
6
+ Run the following command exactly once without changing the current working directory. Replace `{project-root}` with the absolute path to the project root and `{skill-root}` with the absolute path to this skill's directory:
7
7
 
8
8
  ```bash
9
- uv run --no-cache {skill-root}/render.py
9
+ uv run --no-cache "{project-root}/_bmad/scripts/render_skill.py" --project-root "{project-root}" --skill "{skill-root}"
10
10
  ```
11
11
 
12
- - **On success:** follow the instruction it prints to stdout; ignore stderr.
13
- - **On any failure** (including `uv` not being installed): report what it printed and HALT.
12
+ - On success, read and follow the one absolute `workflow.md` instruction printed to stdout.
13
+ - On failure (including `uv` being unavailable), report the command output and HALT. Do not run any workflow source directly.
@@ -20,11 +20,13 @@ The main verification gap shapes are:
20
20
 
21
21
  ### Step 1: Screen for behavioral change
22
22
 
23
- If the change is non-behavioral, stop here and output the clean result (see Output Format). Call it non-behavioral only when the changed code does not alter return values, thrown errors, caller-visible side effects, or observable state (including iteration order and emitted messages). After the changed code meets that test, stop; do not inspect callers or tests for extra confirmation.
23
+ Screen each part of the change separately. If a part is non-behavioral, skip it. Call a part non-behavioral only when the changed code does not alter return values, thrown errors, caller-visible side effects, or observable state (including iteration order and emitted messages). Once a part meets that test, move on; do not inspect callers or tests for extra confirmation.
24
24
 
25
25
  Common non-behavioral examples: formatting, comments, whitespace; pure renames; trivial getters/setters and pass-throughs; type-only or compiler-enforced changes with no runtime effect; etc.
26
26
 
27
- Screen each part of the change separately. Only outcomes produced by deterministic code are worth automatically testing; tests are useless on static source text and brittle on LLM output. Skip those parts and review the rest normally.
27
+ Only outcomes produced by deterministic code are worth automatically testing; tests are useless on static source text and brittle on LLM output. Skip those parts.
28
+
29
+ If every part is skipped, output the clean result (see Output Format).
28
30
 
29
31
  ### Step 2: Find the behavior that changed
30
32
 
@@ -20,7 +20,7 @@ Before listing artifacts or prompting the user, check whether you already know t
20
20
 
21
21
  1. Explicit argument
22
22
  Did the user pass a specific file path, spec name, or clear instruction this message?
23
- - If it points to a file that matches the spec template (has `status` frontmatter with a recognized value: draft, ready-for-dev, in-progress, in-review, or done) → set `spec_file`. Before exiting, run **Story-key resolution** (below). Then **EARLY EXIT** to the appropriate step (step-02 for draft, step-03 for ready/in-progress, step-04 for review). For `done`, ingest as context and proceed to INSTRUCTIONS — do not resume.
23
+ - If it points to a file that matches the spec template (has `status` frontmatter with a recognized value: draft, ready-for-dev, in-progress, in-review, or done) → set `spec_file`. Before exiting, run **Story-key resolution** (below). Then **EARLY EXIT** to the appropriate step: `draft` → `[[bmad-snapshot:step-02-plan.md]]`, `ready-for-dev`/`in-progress` `[[bmad-snapshot:step-03-implement.md]]`, `in-review` → `[[bmad-snapshot:step-04-review.md]]`. For `done`, ingest as context and proceed to INSTRUCTIONS — do not resume.
24
24
  - Anything else (intent files, external docs, plans, descriptions) → ingest it as starting intent and proceed to INSTRUCTIONS. Do not attempt to infer a workflow state from it.
25
25
 
26
26
  2. Recent conversation
@@ -29,9 +29,9 @@ Before listing artifacts or prompting the user, check whether you already know t
29
29
 
30
30
  3. Otherwise — scan artifacts and ask
31
31
  - Active specs (`draft`, `ready-for-dev`, `in-progress`, `in-review`) in `{{.implementation_artifacts}}`? → List them and HALT. Ask user which to resume (or `[N]` for new).
32
- - If `draft` selected: Set `spec_file`. Run **Story-key resolution** (below). **EARLY EXIT** → `./step-02-plan.md` (resume planning from the draft)
33
- - If `ready-for-dev` or `in-progress` selected: Set `spec_file`. Run **Story-key resolution** (below). **EARLY EXIT** → `./step-03-implement.md`
34
- - If `in-review` selected: Set `spec_file`. Run **Story-key resolution** (below). **EARLY EXIT** → `./step-04-review.md`
32
+ - If `draft` selected: Set `spec_file`. Run **Story-key resolution** (below). **EARLY EXIT** → `[[bmad-snapshot:step-02-plan.md]]` (resume planning from the draft)
33
+ - If `ready-for-dev` or `in-progress` selected: Set `spec_file`. Run **Story-key resolution** (below). **EARLY EXIT** → `[[bmad-snapshot:step-03-implement.md]]`
34
+ - If `in-review` selected: Set `spec_file`. Run **Story-key resolution** (below). **EARLY EXIT** → `[[bmad-snapshot:step-04-review.md]]`
35
35
  - Unformatted spec or intent file lacking `status` frontmatter? → Suggest treating its contents as the starting intent. Do NOT attempt to infer a state and resume it.
36
36
 
37
37
  Never ask extra questions if you already understand what the user intends.
@@ -40,7 +40,7 @@ Never ask extra questions if you already understand what the user intends.
40
40
 
41
41
  This runs on ALL paths (early-exit and INSTRUCTIONS) whenever `spec_file` is set. Determine whether the spec is an epic story — use the spec's filename, frontmatter, and any loaded epics file to identify `epic_num` and `story_num`. If the spec is not an epic story, skip silently and leave `story_key` unset.
42
42
 
43
- If the spec is an epic story and `{{.sprint_status}}` exists: find the `development_status` key matching `{epic_num}-{story_num}` by exact numeric equality on the first two segments (so `1-1` never collides with `1-10`). Exactly one match → set `story_key` to that full key. Zero or multiple matches → leave `story_key` unset (warn on multiple).
43
+ If the spec is an epic story and `{{.implementation_artifacts}}/sprint-status.yaml` exists: find the `development_status` key matching `{epic_num}-{story_num}` by exact numeric equality on the first two segments (so `1-1` never collides with `1-10`). Exactly one match → set `story_key` to that full key. Zero or multiple matches → leave `story_key` unset (warn on multiple).
44
44
 
45
45
  ## INSTRUCTIONS
46
46
 
@@ -57,9 +57,9 @@ If the spec is an epic story and `{{.sprint_status}}` exists: find the `developm
57
57
  - **If valid:** load it as the primary planning context. Do not load raw planning docs (PRD, architecture, UX, etc.). Skip to step 5.
58
58
  - **If missing, empty, or invalid:** continue to step 3.
59
59
 
60
- 3. **Compile epic context.** Produce `{{.implementation_artifacts}}/epic-<N>-context.md` by following `./compile-epic-context.md`, in order of preference:
61
- - **Preferred — subagent:** spawn 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`.
62
- - **Fallback — inline** (for runtimes without subagent support, e.g. Copilot, Codex, local Ollama, older Claude): if your runtime cannot spawn subagents, or the spawn fails/times out, read `./compile-epic-context.md` yourself and follow its instructions to produce the same output file.
60
+ 3. **Compile epic context.** Produce `{{.implementation_artifacts}}/epic-<N>-context.md` by following `[[bmad-snapshot:compile-epic-context.md]]`, in order of preference:
61
+ - **Preferred — subagent:** spawn a subagent synchronously (wait for it to return in this turn) with `[[bmad-snapshot: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`.
62
+ - **Fallback — inline** (for runtimes without subagent support, e.g. Copilot, Codex, local Ollama, older Claude): if your runtime cannot spawn subagents, or the spawn fails/times out, read `[[bmad-snapshot:compile-epic-context.md]]` yourself and follow its instructions to produce the same output file.
63
63
 
64
64
  4. **Verify.** After compilation, verify the output file exists, is non-empty, and starts with `# Epic <N> Context:`. If valid, load it. If verification fails, HALT and report the failure.
65
65
 
@@ -81,7 +81,7 @@ If the spec is an epic story and `{{.sprint_status}}` exists: find the `developm
81
81
  - Present detected distinct goals as a bullet list.
82
82
  - Explain briefly (2–4 sentences): why each goal qualifies as independently shippable, any coupling risks if split, and which goal you recommend tackling first.
83
83
  - HALT and ask human: `[S] Split — pick first goal, defer the rest` | `[K] Keep all goals — accept the risks`
84
- - On **S**: For each deferred goal, append one new entry to `{{.deferred_work_file}}` using this format. Do not modify existing entries or look for duplicates. Narrow scope to the first-mentioned goal. Continue routing.
84
+ - On **S**: For each deferred goal, append one new entry to `{{.implementation_artifacts}}/deferred-work.md` using this format. Do not modify existing entries or look for duplicates. Narrow scope to the first-mentioned goal. Continue routing.
85
85
  ```markdown
86
86
  - source_spec: none
87
87
  summary: <one sentence naming the deferred goal>
@@ -90,14 +90,14 @@ If the spec is an epic story and `{{.sprint_status}}` exists: find the `developm
90
90
  - On **K**: Proceed as-is.
91
91
  5. Route — choose exactly one:
92
92
 
93
- 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`.
93
+ 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** → `[[bmad-snapshot:step-02-plan.md]]`); otherwise append `-2`, `-3`, etc. Set `spec_file` = `{{.implementation_artifacts}}/spec-{slug}.md`.
94
94
 
95
95
  **a) One-shot** — zero blast radius: no plausible path by which this change causes unintended consequences elsewhere. Clear intent, no architectural decisions.
96
96
 
97
- **EARLY EXIT** → `./step-oneshot.md`
97
+ **EARLY EXIT** → `[[bmad-snapshot:step-oneshot.md]]`
98
98
 
99
99
  **b) Plan-code-review** — everything else. When uncertain whether blast radius is truly zero, choose this path.
100
100
 
101
101
  ## NEXT
102
102
 
103
- Read fully and follow `./step-02-plan.md`
103
+ Read fully and follow `[[bmad-snapshot:step-02-plan.md]]`
@@ -9,13 +9,13 @@
9
9
 
10
10
  1. Draft resume check. If `{spec_file}` exists with `status: draft`, read it and capture the verbatim `<frozen-after-approval>...</frozen-after-approval>` block as `preserved_intent`. Otherwise `preserved_intent` is empty.
11
11
  2. Investigate codebase. _Isolate deep exploration in synchronous subagents/tasks where available. To prevent context snowballing, instruct subagents to give you distilled summaries only._ Decide which findings actually matter for execution — the specific files, symbols/lines, reuse points, and read-only constraints — and carry those forward for the Code Map. This is where the investigation lands: the spec preserves it so it is never re-narrated to the implementer at dispatch time.
12
- 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. Drain the investigation into the `## Code Map` section — annotated paths, symbol/line anchors, reuse pointers, and read-only evidence — so the spec is the implementer's investigation map and the step-03 handoff need only point at it. If `preserved_intent` is non-empty, replace the `<frozen-after-approval>` block in the spec you just filled out with `preserved_intent`, before writing. Write the result to `{spec_file}`.
12
+ 3. Read `[[bmad-snapshot:spec-template.md]]` fully. Fill it out based on the intent and investigation, resolving the template's `date` field to the current system date. Drain the investigation into the `## Code Map` section — annotated paths, symbol/line anchors, reuse pointers, and read-only evidence — so the spec is the implementer's investigation map and the step-03 handoff need only point at it. If `preserved_intent` is non-empty, replace the `<frozen-after-approval>` block in the spec you just filled out with `preserved_intent`, before writing. Write the result to `{spec_file}`.
13
13
  4. Self-review against READY FOR DEVELOPMENT standard.
14
14
  5. If intent gaps exist, do not fantasize, do not leave open questions, HALT and ask the human.
15
15
  6. Token count check (see SCOPE STANDARD). If spec exceeds 1600 tokens:
16
16
  - Show user the token count.
17
17
  - HALT and ask human: `[S] Split — carve off secondary goals` | `[K] Keep full spec — accept the risks`
18
- - On **S**: Propose the split — name each secondary goal. For each deferred goal, append one new entry to `{{.deferred_work_file}}` using this format. Do not modify existing entries or look for duplicates. Rewrite the current spec to cover only the main goal — do not surgically carve sections out; regenerate the spec for the narrowed scope. Continue to checkpoint.
18
+ - On **S**: Propose the split — name each secondary goal. For each deferred goal, append one new entry to `{{.implementation_artifacts}}/deferred-work.md` using this format. Do not modify existing entries or look for duplicates. Rewrite the current spec to cover only the main goal — do not surgically carve sections out; regenerate the spec for the narrowed scope. Continue to checkpoint.
19
19
  ```markdown
20
20
  - source_spec: `{spec_file}`
21
21
  summary: <one sentence naming the deferred goal>
@@ -44,4 +44,4 @@ HALT and ask human: `[A] Approve` | `[E] Edit`
44
44
 
45
45
  ## NEXT
46
46
 
47
- Read fully and follow `./step-03-implement.md`
47
+ Read fully and follow `[[bmad-snapshot:step-03-implement.md]]`
@@ -24,7 +24,7 @@ Capture `baseline_commit` (current HEAD, or `NO_VCS` if version control is unava
24
24
 
25
25
  Change `{spec_file}` status to `in-progress` in the frontmatter before starting implementation.
26
26
 
27
- Follow `./sync-sprint-status.md` with `target_status` = `in-progress`.
27
+ Follow `[[bmad-snapshot:sync-sprint-status.md]]` with `target_status` = `in-progress`.
28
28
 
29
29
  Execute the implementation handoff below: substitute the runtime placeholders (e.g. `{spec_file}`) into it, then follow it verbatim.
30
30
 
@@ -46,4 +46,4 @@ If `{spec_file}`'s `<frozen-after-approval>` block contains an I/O & Edge-Case M
46
46
 
47
47
  ## NEXT
48
48
 
49
- Read fully and follow `./step-04-review.md`
49
+ Read fully and follow `[[bmad-snapshot:step-04-review.md]]`
@@ -39,10 +39,10 @@ If a layer's instruction requires subagents and none are available, for each suc
39
39
  - **defer** — pre-existing issue not caused by this story, surfaced incidentally by the review. Collect for later focused attention.
40
40
  - **reject** — noise. Drop silently. When unsure between defer and reject, prefer reject — only defer findings you are confident are real.
41
41
  4. Process findings in cascading order. If intent_gap or bad_spec findings exist, they trigger a loopback — lower findings are moot since code will be re-derived. If neither exists, process patch and defer normally. Before each loopback, read `{spec_file}` frontmatter `review_loop_iteration` (missing means `0`), increment it by 1, and write it back. If it exceeds 5, HALT and escalate to the human.
42
- - **intent_gap** — Root cause is inside `<frozen-after-approval>`. Revert code changes. Loop back to the human to resolve. Once resolved, read fully and follow `./step-02-plan.md` to re-run steps 2–4.
43
- - **bad_spec** — Root cause is outside `<frozen-after-approval>`. 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 non-frozen sections 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. Read fully and follow `./step-03-implement.md` to re-derive the code, then this step will run again.
42
+ - **intent_gap** — Root cause is inside `<frozen-after-approval>`. Revert code changes. Loop back to the human to resolve. Once resolved, read fully and follow `[[bmad-snapshot:step-02-plan.md]]` to re-run steps 2–4.
43
+ - **bad_spec** — Root cause is outside `<frozen-after-approval>`. 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 non-frozen sections 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. Read fully and follow `[[bmad-snapshot:step-03-implement.md]]` to re-derive the code, then this step will run again.
44
44
  - **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 checks in `{spec_file}`'s `## Verification` section, if present; if verification fails and the failure cannot be fixed, HALT and escalate to the human.
45
- - **defer** — Append one new entry to `{{.deferred_work_file}}` using this format. Do not modify existing entries or look for duplicates.
45
+ - **defer** — Append one new entry to `{{.implementation_artifacts}}/deferred-work.md` using this format. Do not modify existing entries or look for duplicates.
46
46
  ```markdown
47
47
  - source_spec: `{spec_file}`
48
48
  summary: <one sentence>
@@ -52,4 +52,4 @@ If a layer's instruction requires subagents and none are available, for each suc
52
52
 
53
53
  ## NEXT
54
54
 
55
- Read fully and follow `./step-05-present.md`
55
+ Read fully and follow `[[bmad-snapshot:step-05-present.md]]`
@@ -52,7 +52,7 @@ When there is only one concern, omit the bold label — just list the stops dire
52
52
 
53
53
  Change `{spec_file}` status to `done` in the frontmatter.
54
54
 
55
- Follow `./sync-sprint-status.md` with `target_status` = `review`.
55
+ Follow `[[bmad-snapshot:sync-sprint-status.md]]` with `target_status` = `review`.
56
56
 
57
57
  ### Commit and Complete
58
58
 
@@ -11,7 +11,7 @@
11
11
 
12
12
  ### Implement
13
13
 
14
- Follow `./sync-sprint-status.md` with `target_status` = `in-progress`.
14
+ Follow `[[bmad-snapshot:sync-sprint-status.md]]` with `target_status` = `in-progress`.
15
15
 
16
16
  Implement the clarified intent directly.
17
17
 
@@ -28,7 +28,7 @@ If a layer's instruction requires subagents and none are available, for each suc
28
28
  Deduplicate all review findings. Three categories only:
29
29
 
30
30
  - **patch** — trivially fixable. Auto-fix immediately.
31
- - **defer** — pre-existing issue not caused by this change. Append one new entry to `{{.deferred_work_file}}` using this format. Do not modify existing entries or look for duplicates.
31
+ - **defer** — pre-existing issue not caused by this change. Append one new entry to `{{.implementation_artifacts}}/deferred-work.md` using this format. Do not modify existing entries or look for duplicates.
32
32
  ```markdown
33
33
  - source_spec: `{spec_file}`
34
34
  summary: <one sentence>
@@ -42,13 +42,13 @@ If a finding is caused by this change but too significant for a trivial patch, H
42
42
 
43
43
  Set `title` = a concise title derived from the clarified intent.
44
44
 
45
- Write `{spec_file}` using `./spec-template.md`. Fill only these sections — delete all others:
45
+ Write `{spec_file}` using `[[bmad-snapshot:spec-template.md]]`. Fill only these sections — delete all others:
46
46
 
47
47
  1. **Frontmatter** — set `title: '{title}'`, `type`, `created`, `status: 'done'`. Add `route: 'one-shot'`.
48
48
  2. **Title and Intent** — `# {title}` heading and `## Intent` with **Problem** and **Approach** lines. Reuse the summary you already generated for the terminal.
49
- 3. **Suggested Review Order** — append after Intent. Build using the same convention as `./step-05-present.md` § "Generate Suggested Review Order" (spec-file-relative links, concern-based ordering, ultra-concise framing).
49
+ 3. **Suggested Review Order** — append after Intent. Build using the same convention as `[[bmad-snapshot:step-05-present.md]]` § "Generate Suggested Review Order" (spec-file-relative links, concern-based ordering, ultra-concise framing).
50
50
 
51
- Follow `./sync-sprint-status.md` with `target_status` = `review`.
51
+ Follow `[[bmad-snapshot:sync-sprint-status.md]]` with `target_status` = `review`.
52
52
 
53
53
  ### Commit
54
54
 
@@ -6,11 +6,11 @@ Shared sub-step for updating `sprint-status.yaml` during build. Called from any
6
6
 
7
7
  Skip this entire file (return to caller) if ANY of:
8
8
  - `story_key` is unset
9
- - `{{.sprint_status}}` does not exist on disk
9
+ - `{{.implementation_artifacts}}/sprint-status.yaml` does not exist on disk
10
10
 
11
11
  ## Instructions
12
12
 
13
- 1. Load the FULL `{{.sprint_status}}` file.
13
+ 1. Load the FULL `{{.implementation_artifacts}}/sprint-status.yaml` file.
14
14
  2. Find the `development_status` entry matching `{story_key}`. If not found, warn the user once (`"{story_key} not found in sprint-status; skipping sprint sync"`) and return to caller.
15
15
  3. **Idempotency check.** If `development_status[{story_key}]` is already at `target_status` or a later state (`review` is later than `in-progress`; `done` is later than both), return to caller — no write needed. Never regress a story's status.
16
16
  4. Set `development_status[{story_key}]` to `{target_status}`.
@@ -2,7 +2,7 @@
2
2
 
3
3
  **Goal:** Turn user intent into a hardened, reviewable artifact.
4
4
 
5
- **CRITICAL:** If a step says "read fully and follow step-XX", you read and follow step-XX. No exceptions.
5
+ **CRITICAL:** If a step directs you to another snapshot file, read it fully and follow it. No exceptions.
6
6
 
7
7
  Subagents, when the capability is available, are an important part of this workflow. Use them as directed by the workflow steps.
8
8
  If you need an explicit user instruction to run them, ask once now for the whole workflow run.
@@ -30,10 +30,8 @@ A specification should target a **single user-facing goal** within **900–1600
30
30
 
31
31
  ## Conventions
32
32
 
33
- - Bare paths (e.g. `step-01-clarify-and-route.md`) resolve from the skill root.
34
- - `{skill-root}` resolves to this skill's installed directory (where `customize.toml` lives).
33
+ - Every operational cross-file reference in this workflow is an absolute snapshot path. Open it directly; do not resolve it relative to a skill directory.
35
34
  - `{project-root}`-prefixed paths resolve from the project working directory.
36
- - `{skill-name}` resolves to the skill directory's basename.
37
35
  - Whenever this workflow captures or records a version-control revision, obtain the full canonical identifier directly from version control and preserve it verbatim.
38
36
 
39
37
  ## On Activation
@@ -83,4 +81,4 @@ This uses **step-file architecture** for disciplined execution:
83
81
 
84
82
  ## FIRST STEP
85
83
 
86
- Read fully and follow: `./step-01-clarify-and-route.md` to begin the workflow.
84
+ Read fully and follow: `[[bmad-snapshot:step-01-clarify-and-route.md]]` to begin the workflow.
@@ -3,7 +3,7 @@ name: bmad-build-auto
3
3
  description: 'One iteration of an unattended development loop. Use when invoked by name.'
4
4
  ---
5
5
 
6
- Run this single command exactly once, substituting the absolute project and skill roots without changing the working directory:
6
+ Run the following command exactly once without changing the current working directory. Replace `{project-root}` with the absolute path to the project root and `{skill-root}` with the absolute path to this skill's directory:
7
7
 
8
8
  ```bash
9
9
  uv run --no-cache "{project-root}/_bmad/scripts/render_skill.py" --project-root "{project-root}" --skill "{skill-root}"
@@ -20,11 +20,13 @@ The main verification gap shapes are:
20
20
 
21
21
  ### Step 1: Screen for behavioral change
22
22
 
23
- If the change is non-behavioral, stop here and output the clean result (see Output Format). Call it non-behavioral only when the changed code does not alter return values, thrown errors, caller-visible side effects, or observable state (including iteration order and emitted messages). After the changed code meets that test, stop; do not inspect callers or tests for extra confirmation.
23
+ Screen each part of the change separately. If a part is non-behavioral, skip it. Call a part non-behavioral only when the changed code does not alter return values, thrown errors, caller-visible side effects, or observable state (including iteration order and emitted messages). Once a part meets that test, move on; do not inspect callers or tests for extra confirmation.
24
24
 
25
25
  Common non-behavioral examples: formatting, comments, whitespace; pure renames; trivial getters/setters and pass-throughs; type-only or compiler-enforced changes with no runtime effect; etc.
26
26
 
27
- Screen each part of the change separately. Only outcomes produced by deterministic code are worth automatically testing; tests are useless on static source text and brittle on LLM output. Skip those parts and review the rest normally.
27
+ Only outcomes produced by deterministic code are worth automatically testing; tests are useless on static source text and brittle on LLM output. Skip those parts.
28
+
29
+ If every part is skipped, output the clean result (see Output Format).
28
30
 
29
31
  ### Step 2: Find the behavior that changed
30
32
 
@@ -20,11 +20,13 @@ The main verification gap shapes are:
20
20
 
21
21
  ### Step 1: Screen for behavioral change
22
22
 
23
- If the change is non-behavioral, stop here and output the clean result (see Output Format). Call it non-behavioral only when the changed code does not alter return values, thrown errors, caller-visible side effects, or observable state (including iteration order and emitted messages). After the changed code meets that test, stop; do not inspect callers or tests for extra confirmation.
23
+ Screen each part of the change separately. If a part is non-behavioral, skip it. Call a part non-behavioral only when the changed code does not alter return values, thrown errors, caller-visible side effects, or observable state (including iteration order and emitted messages). Once a part meets that test, move on; do not inspect callers or tests for extra confirmation.
24
24
 
25
25
  Common non-behavioral examples: formatting, comments, whitespace; pure renames; trivial getters/setters and pass-throughs; type-only or compiler-enforced changes with no runtime effect; etc.
26
26
 
27
- Screen each part of the change separately. Only outcomes produced by deterministic code are worth automatically testing; tests are useless on static source text and brittle on LLM output. Skip those parts and review the rest normally.
27
+ Only outcomes produced by deterministic code are worth automatically testing; tests are useless on static source text and brittle on LLM output. Skip those parts.
28
+
29
+ If every part is skipped, output the clean result (see Output Format).
28
30
 
29
31
  ### Step 2: Find the behavior that changed
30
32
 
@@ -177,7 +177,8 @@ def _format_review_layers(layers: list[dict[str, str]]) -> str:
177
177
 
178
178
  def _resolve_customization_value(value: Any, default: Any, label: str) -> tuple[Any, str]:
179
179
  if isinstance(default, str):
180
- resolved = _require_string(value, label, allow_empty=not default.strip())
180
+ allow_empty = not default.strip() or label == "customization.workflow.open_spec"
181
+ resolved = _require_string(value, label, allow_empty=allow_empty)
181
182
  return resolved, resolved
182
183
  if isinstance(default, list):
183
184
  if default and all(isinstance(item, dict) for item in default):
@@ -1,420 +0,0 @@
1
- #!/usr/bin/env python3
2
- # /// script
3
- # requires-python = ">=3.11"
4
- # ///
5
- """render.py — bmad-build template renderer.
6
-
7
- Resolves compile-time {{.variable}} placeholders from BMad's central config,
8
- bakes absolute paths for {project-root} into derived values, resolves and
9
- inlines the skill's [workflow] customization block, and writes rendered .md
10
- files to {project-root}/_bmad/render/bmad-build/.
11
-
12
- Config: four-layer merge of _bmad/config.toml + config.user.toml +
13
- custom/config.toml + custom/config.user.toml (post-#2285 installs).
14
- Keys surface from [core] and [modules.bmm]. Missing or unparseable
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.
19
-
20
- Customization: three-layer merge of {skill}/customize.toml +
21
- _bmad/custom/bmad-build.toml + .user.toml (same structural rules as
22
- resolve_customization.py). The resolved [workflow] values fill {workflow.*}
23
- placeholders, so this skill needs no runtime resolve_customization.py call.
24
- Other single-curly placeholders ({project-root}, {spec_file}, ...) pass
25
- through untouched for the LLM to resolve during workflow execution. The sole
26
- exception is {skill-root} in resolved review-layer instructions, which must be
27
- an absolute prompt-file path before those instructions reach the parent LLM.
28
-
29
- Every invocation rebuilds from scratch — no hash, no cache.
30
- Python 3.11+ stdlib only. UTF-8 I/O.
31
- """
32
-
33
- import os
34
- import posixpath
35
- import re
36
- import sys
37
- import tomllib
38
-
39
-
40
- def find_project_root():
41
- """Walk up from cwd until a _bmad/ directory is found. On failure, print a
42
- HALT instruction to stdout and exit non-zero."""
43
- current = os.path.abspath(os.getcwd())
44
- while True:
45
- candidate = os.path.join(current, "_bmad")
46
- if os.path.isdir(candidate):
47
- return current
48
- parent = os.path.dirname(current)
49
- if parent == current:
50
- print(
51
- f"HALT and report to the user: no _bmad/ directory found walking up from {os.getcwd()}"
52
- )
53
- sys.exit(1)
54
- current = parent
55
-
56
-
57
- def load_toml(path, required=False):
58
- """Load a TOML file. Only absence is negotiable: a missing optional file
59
- returns {} (customization layers are optional), a missing required file
60
- HALTs. A file that exists but cannot be parsed or read always HALTs —
61
- stdout is how this script signals workflow halts to its LLM caller — the
62
- user wrote it to be honored, and silently continuing with {} would discard
63
- their customizations with no failure signal."""
64
- if not os.path.isfile(path):
65
- if required:
66
- print(
67
- f"HALT and report to the user: required config file not found: {path} — "
68
- "ensure this is a post-#2285 BMAD install"
69
- )
70
- sys.exit(1)
71
- return {}
72
- try:
73
- with open(path, "rb") as fh:
74
- parsed = tomllib.load(fh)
75
- except tomllib.TOMLDecodeError as error:
76
- print(f"HALT and report to the user: failed to parse {path}: {error}")
77
- sys.exit(1)
78
- except OSError as error:
79
- print(f"HALT and report to the user: failed to read {path}: {error}")
80
- sys.exit(1)
81
- if not isinstance(parsed, dict):
82
- return {}
83
- return parsed
84
-
85
-
86
- def _deep_merge(base, override):
87
- """Dict-aware deep merge. Lists and scalars: override wins (we don't need
88
- the full keyed-merge semantics of resolve_config.py — build only reads
89
- flat scalars out of [core] and [modules.bmm])."""
90
- if isinstance(base, dict) and isinstance(override, dict):
91
- result = dict(base)
92
- for key, value in override.items():
93
- result[key] = _deep_merge(result[key], value) if key in result else value
94
- return result
95
- return override
96
-
97
-
98
- def _detect_keyed_merge_field(items):
99
- """Return 'code' or 'id' if every table item carries that same field.
100
- Mixed or partial arrays return None and fall through to append."""
101
- if not items or not all(isinstance(item, dict) for item in items):
102
- return None
103
- for candidate in ("code", "id"):
104
- if all(item.get(candidate) is not None for item in items):
105
- return candidate
106
- return None
107
-
108
-
109
- def _merge_by_key(base, override, key_name):
110
- result = []
111
- index_by_key = {}
112
- for item in base:
113
- if not isinstance(item, dict):
114
- continue
115
- if item.get(key_name) is not None:
116
- index_by_key[item[key_name]] = len(result)
117
- result.append(dict(item))
118
- for item in override:
119
- if not isinstance(item, dict):
120
- result.append(item)
121
- continue
122
- key = item.get(key_name)
123
- if key is not None and key in index_by_key:
124
- result[index_by_key[key]] = dict(item)
125
- else:
126
- if key is not None:
127
- index_by_key[key] = len(result)
128
- result.append(dict(item))
129
- return result
130
-
131
-
132
- def _merge_arrays(base, override):
133
- """Shape-aware array merge: keyed merge if every item has code/id, else append."""
134
- base_arr = base if isinstance(base, list) else []
135
- override_arr = override if isinstance(override, list) else []
136
- keyed_field = _detect_keyed_merge_field(base_arr + override_arr)
137
- if keyed_field:
138
- return _merge_by_key(base_arr, override_arr, keyed_field)
139
- return base_arr + override_arr
140
-
141
-
142
- def _structural_merge(base, override):
143
- """Faithful port of resolve_customization.py's deep_merge: tables deep-merge,
144
- arrays-of-tables keyed by code/id replace-then-append (other arrays append),
145
- scalars override. Used only for the [workflow] customization layers — the
146
- central-config path keeps its own simpler _deep_merge. Duplicated rather than
147
- imported to keep this skill self-contained."""
148
- if isinstance(base, dict) and isinstance(override, dict):
149
- result = dict(base)
150
- for key, over_val in override.items():
151
- result[key] = (
152
- _structural_merge(result[key], over_val) if key in result else over_val
153
- )
154
- return result
155
- if isinstance(base, list) and isinstance(override, list):
156
- return _merge_arrays(base, override)
157
- return override
158
-
159
-
160
- def resolve_workflow(root, skill_dir, skill_name):
161
- """Resolve the [workflow] customization block via the three-layer merge
162
- (skill defaults -> team -> user), highest priority last. Same structural
163
- rules as resolve_customization.py. All three layers are optional: a missing
164
- file is skipped, but an unparseable one HALTs (via load_toml)."""
165
- defaults = load_toml(posixpath.join(skill_dir, "customize.toml"))
166
- custom_dir = posixpath.join(root, "_bmad", "custom")
167
- team = load_toml(posixpath.join(custom_dir, f"{skill_name}.toml"))
168
- user = load_toml(posixpath.join(custom_dir, f"{skill_name}.user.toml"))
169
- merged = _structural_merge(defaults, team)
170
- merged = _structural_merge(merged, user)
171
- workflow = merged.get("workflow")
172
- return workflow if isinstance(workflow, dict) else {}
173
-
174
-
175
- def load_central_config(root):
176
- """Four-layer merge of _bmad/config.toml and its peers (highest priority
177
- last). HALTs if the base _bmad/config.toml is missing or unparseable."""
178
- bmad_dir = posixpath.join(root, "_bmad")
179
- base_team = load_toml(posixpath.join(bmad_dir, "config.toml"), required=True)
180
- base_user = load_toml(posixpath.join(bmad_dir, "config.user.toml"))
181
- custom_team = load_toml(posixpath.join(bmad_dir, "custom", "config.toml"))
182
- custom_user = load_toml(posixpath.join(bmad_dir, "custom", "config.user.toml"))
183
-
184
- merged = _deep_merge(base_team, base_user)
185
- merged = _deep_merge(merged, custom_team)
186
- merged = _deep_merge(merged, custom_user)
187
- return merged
188
-
189
-
190
- def flatten_central_config(merged):
191
- """Lift scalar keys from [core] and [modules.bmm] into a single namespace.
192
- Module keys take precedence on collision (installer strips core keys from
193
- module buckets, so collisions shouldn't happen in practice)."""
194
- flat = {}
195
- modules = merged.get("modules")
196
- modules = modules if isinstance(modules, dict) else {}
197
- for section in (merged.get("core"), modules.get("bmm")):
198
- if not isinstance(section, dict):
199
- continue
200
- for key, value in section.items():
201
- if isinstance(value, bool):
202
- flat[key] = "true" if value else "false"
203
- elif isinstance(value, (str, int, float)):
204
- flat[key] = str(value)
205
- return flat
206
-
207
-
208
- def render_template(content, vars_):
209
- """Resolve {{.var}} substitutions. Unresolved references emit an empty string,
210
- but main() HALTs on any missing reference before rendering starts, so this
211
- fallback never fires in practice."""
212
- return re.sub(r"\{\{\.(\w+)\}\}", lambda m: vars_.get(m.group(1), ""), content)
213
-
214
-
215
- def collect_missing_vars(sources, vars_):
216
- """Map each {{.var}} name referenced by the source .md files but absent from
217
- the merged config to the files that reference it. A missing key must HALT:
218
- missingkey=zero rendering would bake a corrupted workflow (empty paths,
219
- blank language lines) with no failure signal."""
220
- missing = {}
221
- for fname, content in sources:
222
- for name in re.findall(r"\{\{\.(\w+)\}\}", content):
223
- if name not in vars_:
224
- files = missing.setdefault(name, [])
225
- if fname not in files:
226
- files.append(fname)
227
- return missing
228
-
229
-
230
- def _scalar_str(value):
231
- """Stringify a scalar for inline rendering: booleans lowercase (matching
232
- BMad config conventions), None as empty, everything else via str()."""
233
- if value is None:
234
- return ""
235
- if isinstance(value, bool):
236
- return "true" if value else "false"
237
- return str(value)
238
-
239
-
240
- # [workflow] keys holding review layers ([[workflow.review_layers]] tables with
241
- # id/name/instruction/when fields). This renderer knows this skill's
242
- # customization schema outright — layer semantics are materialized here, not
243
- # interpreted by the LLM at run time.
244
- _REVIEW_LAYER_KEYS = ("review_layers", "oneshot_review_layers")
245
-
246
-
247
- def expand_review_layer_skill_roots(workflow, skill_root):
248
- """Expand only {skill-root} inside resolved review-layer instructions.
249
-
250
- Review layers originate in customization after regular template rendering,
251
- so their prompt paths need this explicit pass. Runtime placeholders remain
252
- untouched for the parent to supply to each child at dispatch time.
253
- """
254
- expanded_workflow = dict(workflow)
255
- for key in _REVIEW_LAYER_KEYS:
256
- layers = workflow.get(key)
257
- if not isinstance(layers, list):
258
- continue
259
- expanded_layers = []
260
- for layer in layers:
261
- if not isinstance(layer, dict):
262
- expanded_layers.append(layer)
263
- continue
264
- expanded_layer = dict(layer)
265
- instruction = expanded_layer.get("instruction")
266
- if isinstance(instruction, str):
267
- expanded_layer["instruction"] = instruction.replace(
268
- "{skill-root}", skill_root
269
- )
270
- expanded_layers.append(expanded_layer)
271
- expanded_workflow[key] = expanded_layers
272
- return expanded_workflow
273
-
274
-
275
- def _render_review_layers(layers):
276
- """Materialize review layers into direct invocation blocks. A layer with an
277
- empty or missing instruction is disabled (that is how an override turns off
278
- a default layer) and drops out entirely. A `when` condition is the one part
279
- that stays with the LLM: it renders as a run-time guard line. No active
280
- layers renders as the HALT instruction the workflow would otherwise have to
281
- derive from an empty list."""
282
- active = [
283
- layer
284
- for layer in layers
285
- if isinstance(layer, dict) and _scalar_str(layer.get("instruction")).strip()
286
- ]
287
- if not active:
288
- return (
289
- "No review layers are active. HALT with status `blocked` and "
290
- "blocking condition `no active review layers`."
291
- )
292
- blocks = []
293
- for layer in active:
294
- title = (
295
- _scalar_str(layer.get("name")).strip()
296
- or _scalar_str(layer.get("id")).strip()
297
- or "Review layer"
298
- )
299
- lines = [f"#### {title}", ""]
300
- when = _scalar_str(layer.get("when")).strip()
301
- if when:
302
- lines.append(
303
- "Run this layer only if the following holds in the "
304
- f"current context: `{when}`"
305
- )
306
- lines.append("")
307
- lines.append(_scalar_str(layer.get("instruction")).strip("\n"))
308
- blocks.append("\n".join(lines))
309
- return "\n\n".join(blocks)
310
-
311
-
312
- def _render_workflow_value(key, value):
313
- """Format a resolved [workflow] value for inline substitution. Review-layer
314
- keys materialize as invocation blocks; other lists render as markdown
315
- bullets (empty -> '_None._'); scalars render verbatim. Each list item uses
316
- the same scalar formatting so booleans stay consistent. Entries are emitted
317
- as-is so runtime placeholders like {project-root} or {diff_output} survive
318
- for the LLM to resolve."""
319
- if key in _REVIEW_LAYER_KEYS and isinstance(value, list):
320
- return _render_review_layers(value)
321
- if isinstance(value, list):
322
- if not value:
323
- return "_None._"
324
- return "\n".join(f"- {_scalar_str(item)}" for item in value)
325
- return _scalar_str(value)
326
-
327
-
328
- def render_workflow(content, workflow):
329
- """Resolve {workflow.<key>} placeholders from the resolved [workflow] block.
330
- Unknown keys emit an empty string (missingkey=zero, matching render_template).
331
- Distinct regex from render_template so single-curly runtime placeholders
332
- elsewhere are untouched."""
333
- return re.sub(
334
- r"\{workflow\.(\w+)\}",
335
- lambda m: _render_workflow_value(m.group(1), workflow.get(m.group(1))),
336
- content,
337
- )
338
-
339
-
340
- def main():
341
- script_dir = os.path.dirname(os.path.abspath(__file__))
342
- skill_name = os.path.basename(script_dir)
343
- root = find_project_root()
344
- root = root.replace(os.sep, "/")
345
-
346
- vars_ = flatten_central_config(load_central_config(root))
347
-
348
- for key in list(vars_.keys()):
349
- vars_[key] = vars_[key].replace("{project-root}", root)
350
-
351
- vars_["project_root"] = root
352
-
353
- # Guarded ahead of the general missing-vars scan: sprint_status and
354
- # deferred_work_file derive from it below, and unlike the scan (absent
355
- # keys only) this also HALTs on a present-but-empty value.
356
- implementation_artifacts = vars_.get("implementation_artifacts", "").strip()
357
- if not implementation_artifacts:
358
- print(
359
- "HALT and report to the user: config is missing `implementation_artifacts` "
360
- "(expected under [core] or [modules.bmm] in _bmad/config.toml)"
361
- )
362
- sys.exit(1)
363
-
364
- vars_["sprint_status"] = posixpath.join(
365
- implementation_artifacts, "sprint-status.yaml"
366
- )
367
- vars_["deferred_work_file"] = posixpath.join(
368
- implementation_artifacts, "deferred-work.md"
369
- )
370
-
371
- sources = []
372
- for fname in sorted(os.listdir(script_dir)):
373
- if not fname.endswith(".md") or fname == "SKILL.md":
374
- continue
375
- with open(
376
- posixpath.join(script_dir, fname), "r", encoding="utf-8", newline=""
377
- ) as fh:
378
- sources.append((fname, fh.read()))
379
-
380
- missing = collect_missing_vars(sources, vars_)
381
- if missing:
382
- details = "; ".join(
383
- f"`{name}` (referenced by {', '.join(files)})"
384
- for name, files in sorted(missing.items())
385
- )
386
- print(
387
- f"HALT and report to the user: config is missing {details} "
388
- "(expected under [core] or [modules.bmm] in _bmad/config.toml)"
389
- )
390
- sys.exit(1)
391
-
392
- skill_dir = script_dir.replace(os.sep, "/")
393
- workflow = resolve_workflow(root, skill_dir, skill_name)
394
- open_spec = workflow.get("open_spec")
395
- if not isinstance(open_spec, str):
396
- print(
397
- "HALT and report to the user: customization `workflow.open_spec` "
398
- "must be a string"
399
- )
400
- sys.exit(1)
401
- workflow = expand_review_layer_skill_roots(workflow, skill_dir)
402
-
403
- out_dir = posixpath.join(root, "_bmad", "render", skill_name)
404
- os.makedirs(out_dir, exist_ok=True)
405
-
406
- for fname in os.listdir(out_dir):
407
- if fname.endswith(".md"):
408
- os.remove(posixpath.join(out_dir, fname))
409
-
410
- for fname, content in sources:
411
- dst = posixpath.join(out_dir, fname)
412
- with open(dst, "w", encoding="utf-8", newline="") as fh:
413
- fh.write(render_workflow(render_template(content, vars_), workflow))
414
-
415
- workflow_md = posixpath.join(out_dir, "workflow.md")
416
- print(f"read and follow {workflow_md}")
417
-
418
-
419
- if __name__ == "__main__":
420
- main()