mirror of
https://github.com/bmad-code-org/BMAD-METHOD.git
synced 2026-08-28 19:20:41 +08:00
fix(installer,skills): make uv a real requirement and stop assuming a system Python (#2704)
The installer told users uv was optional while bmad-build had already made it mandatory. uv-check.js called it "becoming the de facto standard", install-messages.yaml led with HEADS UP, and installer.js printed a Tip inside a box titled "BMAD is ready to use!" — while bmad-build and bmad-build-auto HALT on activation without `uv run`. The probe's result was discarded (`await checkUvEnvironment();`), so nothing branched on it. Messaging now names the consequence, and the post-install summary repeats the warning when it applies — the pre-install probe fires before every prompt, so by then it is far up the scrollback. Still warn-don't-block: core-only, docs-only, and CI installs never render a skill, so a missing uv must not fail the run. Adds a python3 probe used only when uv is absent, since that is the only case where the interpreter on PATH matters. It reports whether the direct-interpreter skills still work (3.11+) or nothing Python-backed will (below 3.11, or no python3 at all). Separately, 25 call sites still ran resolve_customization.py under a bare `python3`. That script requires 3.11+ for tomllib, so on macOS without Homebrew or Ubuntu 22.04 they fell through to their "if the script fails" path and hand-merged the TOML in-context — no error surfaced. All 25 now use `uv run`, which provisions a matching interpreter from the script's own requires-python. Four more spawned Python purely to open an HTML file: python3 -c "import webbrowser, pathlib; webbrowser.open(...)" Replaced with the platform opener bmad-brainstorming already uses — open / xdg-open / start. src/ now contains no bare Python invocation at all, so "Python 3.11+" leaves the user contract: uv provisions its own. docs/how-to/customize-bmad.md described a transition that this ends. Test suite 46 grows from 12 to 29 assertions: Python parsing, the 3.11 boundary in both directions, that uv-present skips the python3 probe, and all three missing-uv sub-branches.
This commit is contained in:
@@ -22,7 +22,7 @@ The `bmad-customize` skill is a guided authoring helper for the **per-skill agen
|
||||
:::note[Prerequisites]
|
||||
|
||||
- BMad installed in your project (see [How to Install BMad](./install-bmad.md))
|
||||
- A way to run the resolver script — BMad is standardizing on `uv` (`uv run`, which provisions Python for you); a plain `python3` 3.11+ on your PATH still works during the transition. The script uses only stdlib `tomllib`, so there's nothing to `pip install`.
|
||||
- [`uv`](https://docs.astral.sh/uv/) on your PATH — BMad runs the resolver script with `uv run`, and uv provisions a suitable Python for you, so you don't need to install one yourself. The script uses only stdlib `tomllib`, so there's nothing to `pip install`.
|
||||
- A text editor for TOML files
|
||||
:::
|
||||
|
||||
@@ -201,7 +201,7 @@ persistent_facts = [
|
||||
|
||||
## How Resolution Works
|
||||
|
||||
On activation, the agent's SKILL.md runs a shared Python script that does the three-layer merge and returns the resolved block as JSON. The script uses only the Python standard library's `tomllib` module (no external dependencies). BMad is standardizing on `uv run` to invoke these scripts (uv provisions a suitable Python for you); a plain `python3` still works during the transition:
|
||||
On activation, the agent's SKILL.md runs a shared Python script that does the three-layer merge and returns the resolved block as JSON. The script uses only the Python standard library's `tomllib` module (no external dependencies). BMad invokes it with `uv run`, which provisions a suitable Python for you:
|
||||
|
||||
```bash
|
||||
uv run {project-root}/_bmad/scripts/resolve_customization.py \
|
||||
@@ -209,7 +209,7 @@ uv run {project-root}/_bmad/scripts/resolve_customization.py \
|
||||
--key agent
|
||||
```
|
||||
|
||||
**Requirements**: Python 3.11+ (earlier versions don't include `tomllib`); nothing to `pip install`. Running via `uv run` is the going-forward standard — uv resolves a suitable interpreter for you. If you run it with `python3` directly during the transition, check your version with `python3 --version`: some platforms (macOS without Homebrew, Ubuntu 22.04) default `python3` to 3.10 or earlier, so you may need to install 3.11+ separately.
|
||||
**Requirements**: `uv`, and nothing to `pip install`. The script declares `requires-python = ">=3.11"` in its own header (earlier versions don't include `tomllib`), and `uv run` reads that and resolves a matching interpreter — so whatever `python3` resolves to on your PATH doesn't matter. If you'd rather invoke it by hand with `python3`, check your version first: some platforms (macOS without Homebrew, Ubuntu 22.04) default `python3` to 3.10 or earlier.
|
||||
|
||||
`--skill` points at the skill's installed directory (where `customize.toml` lives). The skill name is derived from the directory's basename, and the script looks up `_bmad/custom/{skill-name}.toml` and `{skill-name}.user.toml` automatically.
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ You are Mary, the Business Analyst. You bring deep expertise in market research,
|
||||
|
||||
### Step 1: Resolve the Agent Block
|
||||
|
||||
Run: `python3 {project-root}/_bmad/scripts/resolve_customization.py --skill {skill-root} --key agent`
|
||||
Run: `uv run {project-root}/_bmad/scripts/resolve_customization.py --skill {skill-root} --key agent`
|
||||
|
||||
**If the script fails**, resolve the `agent` block yourself by reading these three files in base → team → user order and applying the same structural merge rules as the resolver:
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ You are Winston, the System Architect. You turn product requirements and UX into
|
||||
|
||||
### Step 1: Resolve the Agent Block
|
||||
|
||||
Run: `python3 {project-root}/_bmad/scripts/resolve_customization.py --skill {skill-root} --key agent`
|
||||
Run: `uv run {project-root}/_bmad/scripts/resolve_customization.py --skill {skill-root} --key agent`
|
||||
|
||||
**If the script fails**, resolve the `agent` block yourself by reading these three files in base → team → user order and applying the same structural merge rules as the resolver:
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ You are Amelia, the Senior Software Engineer. You execute approved stories with
|
||||
|
||||
### Step 1: Resolve the Agent Block
|
||||
|
||||
Run: `python3 {project-root}/_bmad/scripts/resolve_customization.py --skill {skill-root} --key agent`
|
||||
Run: `uv run {project-root}/_bmad/scripts/resolve_customization.py --skill {skill-root} --key agent`
|
||||
|
||||
**If the script fails**, resolve the `agent` block yourself by reading these three files in base → team → user order and applying the same structural merge rules as the resolver:
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ You are John, the Product Manager. You drive PRD creation through user interview
|
||||
|
||||
### Step 1: Resolve the Agent Block
|
||||
|
||||
Run: `python3 {project-root}/_bmad/scripts/resolve_customization.py --skill {skill-root} --key agent`
|
||||
Run: `uv run {project-root}/_bmad/scripts/resolve_customization.py --skill {skill-root} --key agent`
|
||||
|
||||
**If the script fails**, resolve the `agent` block yourself by reading these three files in base → team → user order and applying the same structural merge rules as the resolver:
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ You are Sally, the UX Designer. You translate user needs into interaction design
|
||||
|
||||
### Step 1: Resolve the Agent Block
|
||||
|
||||
Run: `python3 {project-root}/_bmad/scripts/resolve_customization.py --skill {skill-root} --key agent`
|
||||
Run: `uv run {project-root}/_bmad/scripts/resolve_customization.py --skill {skill-root} --key agent`
|
||||
|
||||
**If the script fails**, resolve the `agent` block yourself by reading these three files in base → team → user order and applying the same structural merge rules as the resolver:
|
||||
|
||||
|
||||
@@ -51,7 +51,7 @@ This uses **step-file architecture** for disciplined execution:
|
||||
|
||||
### Step 1: Resolve the Workflow Block
|
||||
|
||||
Run: `python3 {project-root}/_bmad/scripts/resolve_customization.py --skill {skill-root} --key workflow`
|
||||
Run: `uv run {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:
|
||||
|
||||
|
||||
@@ -138,6 +138,6 @@ Upon Completion of task output: offer to answer any questions about the Epics an
|
||||
|
||||
## On Complete
|
||||
|
||||
Run: `python3 {project-root}/_bmad/scripts/resolve_customization.py --skill {skill-root} --key workflow.on_complete`
|
||||
Run: `uv run {project-root}/_bmad/scripts/resolve_customization.py --skill {skill-root} --key workflow.on_complete`
|
||||
|
||||
If the resolved `workflow.on_complete` is non-empty, follow it as the final terminal instruction before exiting.
|
||||
|
||||
@@ -41,11 +41,11 @@ Once every selected reviewer has returned, the parent synthesizes one consolidat
|
||||
- **Footer.** Rubric path, ISO timestamp.
|
||||
3. Write the filled HTML to `{doc_workspace}/validation-report.html`.
|
||||
4. Write the markdown twin to `{doc_workspace}/validation-report.md` (same content, grouped by severity rather than by dimension — see format below; this is the canonical form for downstream re-reading).
|
||||
5. Open the HTML in the default browser:
|
||||
5. Open the HTML in the default browser with the platform opener — `open` on macOS, `xdg-open` on Linux, `start ""` on Windows — double-quoting the path:
|
||||
```bash
|
||||
python3 -c "import webbrowser, pathlib; webbrowser.open(pathlib.Path('{doc_workspace}/validation-report.html').resolve().as_uri())"
|
||||
open "{doc_workspace}/validation-report.html"
|
||||
```
|
||||
Skip the open step in headless mode (see `references/headless.md`).
|
||||
If the command fails, don't retry with another opener: tell the user the file path and move on. Skip the open step in headless mode (see `references/headless.md`).
|
||||
|
||||
### Markdown twin format
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ The PRFAQ forces customer-first clarity: write the press release announcing the
|
||||
|
||||
### Step 1: Resolve the Workflow Block
|
||||
|
||||
Run: `python3 {project-root}/_bmad/scripts/resolve_customization.py --skill {skill-root} --key workflow`
|
||||
Run: `uv run {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:
|
||||
|
||||
|
||||
@@ -78,6 +78,6 @@ purpose: "Token-efficient context for downstream PRD creation"
|
||||
|
||||
This is the terminal stage. If the user wants to revise, loop back to the relevant stage. Otherwise, the workflow is done.
|
||||
|
||||
Run: `python3 {project-root}/_bmad/scripts/resolve_customization.py --skill {skill-root} --key workflow.on_complete`
|
||||
Run: `uv run {project-root}/_bmad/scripts/resolve_customization.py --skill {skill-root} --key workflow.on_complete`
|
||||
|
||||
If the resolved `workflow.on_complete` is non-empty, follow it as the final terminal instruction before exiting.
|
||||
|
||||
@@ -6,4 +6,4 @@ Each variation: header (name + one-line emotional register), token chips for eve
|
||||
|
||||
Inline CSS only, system font stack, no JS, no network. Document concrete hex values in `<style>` comments per variation so the user can lift them if they pick that theme. The spine itself stays semantic.
|
||||
|
||||
Return to the parent: file path, one-line per variation, mode coverage. Do not dump HTML into the parent context. If interactive, open the file with `python3 -c "import webbrowser, pathlib; webbrowser.open(pathlib.Path('PATH').resolve().as_uri())"`.
|
||||
Return to the parent: file path, one-line per variation, mode coverage. Do not dump HTML into the parent context. If interactive, open the file with the platform opener — `open "PATH"` on macOS, `xdg-open` on Linux, `start ""` on Windows, path always double-quoted. On failure, give the user the path instead.
|
||||
|
||||
@@ -16,4 +16,4 @@ Every renderer writes to `{doc_workspace}/.working/` with a descriptive filename
|
||||
|
||||
The parent passes the subagent: current `.memlog.md`, relevant prior `.working/` captures, the user's stated intent for this pass, the output path. The subagent writes its artifact under `.working/` and returns ONLY a compact summary (file path, one line per variant, mode coverage). Parent never holds the full payload.
|
||||
|
||||
For HTML, open in browser when interactive: `python3 -c "import webbrowser, pathlib; webbrowser.open(pathlib.Path('PATH').resolve().as_uri())"`. Skip in headless.
|
||||
For HTML, open in the browser when interactive with the platform opener — `open "PATH"` on macOS, `xdg-open` on Linux, `start ""` on Windows, path always double-quoted. On failure, give the user the path instead. Skip in headless.
|
||||
|
||||
@@ -69,7 +69,7 @@ Under Validate intent, after every reviewer returns, render one consolidated rep
|
||||
2. Fill `{workflow.validation_report_template}`. No overall grade — the per-category verdicts and severity counts already say what's true. Synthesis paragraph lifts the rubric's overall verdict; add a second if extra reviewers shift the picture. One section per rubric category (open if thin / broken), one per extra reviewer (closed, adversarial voice preserved).
|
||||
3. Write `{doc_workspace}/validation-report.html`.
|
||||
4. Write the Markdown twin `{doc_workspace}/validation-report.md` — same content grouped by severity.
|
||||
5. Open HTML: `python3 -c "import webbrowser, pathlib; webbrowser.open(pathlib.Path('{doc_workspace}/validation-report.html').resolve().as_uri())"`. Skip headless.
|
||||
5. Open HTML with the platform opener — `open "{doc_workspace}/validation-report.html"` on macOS, `xdg-open` on Linux, `start ""` on Windows, path always double-quoted. On failure, give the user the path instead. Skip headless.
|
||||
|
||||
Re-running overwrites the consolidated report; individual `review-*.md` files persist.
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ description: 'LLM-assisted human-in-the-loop review. Make sense of a change, foc
|
||||
|
||||
### Step 1: Resolve the Workflow Block
|
||||
|
||||
Run: `python3 {project-root}/_bmad/scripts/resolve_customization.py --skill {skill-root} --key workflow`
|
||||
Run: `uv run {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:
|
||||
|
||||
|
||||
@@ -25,6 +25,6 @@ HALT — do not proceed until the user makes their choice.
|
||||
|
||||
## On Complete
|
||||
|
||||
Run: `python3 {project-root}/_bmad/scripts/resolve_customization.py --skill {skill-root} --key workflow.on_complete`
|
||||
Run: `uv run {project-root}/_bmad/scripts/resolve_customization.py --skill {skill-root} --key workflow.on_complete`
|
||||
|
||||
If the resolved `workflow.on_complete` is non-empty, follow it as the final terminal instruction before exiting.
|
||||
|
||||
@@ -21,7 +21,7 @@ If you need an explicit user instruction to run them, ask once now for the whole
|
||||
|
||||
### Step 1: Resolve the Workflow Block
|
||||
|
||||
Run: `python3 {project-root}/_bmad/scripts/resolve_customization.py --skill {skill-root} --key workflow`
|
||||
Run: `uv run {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:
|
||||
|
||||
|
||||
@@ -127,6 +127,6 @@ Present the user with follow-up options:
|
||||
|
||||
## On Complete
|
||||
|
||||
Run: `python3 {project-root}/_bmad/scripts/resolve_customization.py --skill {skill-root} --key workflow.on_complete`
|
||||
Run: `uv run {project-root}/_bmad/scripts/resolve_customization.py --skill {skill-root} --key workflow.on_complete`
|
||||
|
||||
If the resolved `workflow.on_complete` is non-empty, follow it as the final terminal instruction before exiting.
|
||||
|
||||
@@ -20,7 +20,7 @@ description: 'Manage significant changes during sprint execution. Use when the u
|
||||
|
||||
### Step 1: Resolve the Workflow Block
|
||||
|
||||
Run: `python3 {project-root}/_bmad/scripts/resolve_customization.py --skill {skill-root} --key workflow`
|
||||
Run: `uv run {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:
|
||||
|
||||
@@ -294,7 +294,7 @@ Activation is complete. If `activation_steps_prepend` or `activation_steps_appen
|
||||
|
||||
<action>Report workflow completion to user with personalized message: "Correct Course workflow complete, {user_name}!"</action>
|
||||
<action>Remind user of success criteria and next steps for Developer agent</action>
|
||||
<action>Run: `python3 {project-root}/_bmad/scripts/resolve_customization.py --skill {skill-root} --key workflow.on_complete` — if the resolved value is non-empty, follow it as the final terminal instruction before exiting.</action>
|
||||
<action>Run: `uv run {project-root}/_bmad/scripts/resolve_customization.py --skill {skill-root} --key workflow.on_complete` — if the resolved value is non-empty, follow it as the final terminal instruction before exiting.</action>
|
||||
</step>
|
||||
|
||||
</workflow>
|
||||
|
||||
@@ -20,7 +20,7 @@ description: 'Generate end to end automated tests for existing features. Use whe
|
||||
|
||||
### Step 1: Resolve the Workflow Block
|
||||
|
||||
Run: `python3 {project-root}/_bmad/scripts/resolve_customization.py --skill {skill-root} --key workflow`
|
||||
Run: `uv run {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:
|
||||
|
||||
@@ -171,6 +171,6 @@ Save summary to: `{default_output_file}`
|
||||
|
||||
## On Complete
|
||||
|
||||
Run: `python3 {project-root}/_bmad/scripts/resolve_customization.py --skill {skill-root} --key workflow.on_complete`
|
||||
Run: `uv run {project-root}/_bmad/scripts/resolve_customization.py --skill {skill-root} --key workflow.on_complete`
|
||||
|
||||
If the resolved `workflow.on_complete` is non-empty, follow it as the final terminal instruction before exiting.
|
||||
|
||||
@@ -9,7 +9,7 @@ This skill was consolidated into `bmad-architecture`. It is retained as a thin c
|
||||
|
||||
## On Activation
|
||||
|
||||
1. Resolve customization: `python3 {project-root}/_bmad/scripts/resolve_customization.py --skill {skill-root} --key workflow`. This picks up any `{project-root}/_bmad/custom/bmad-create-architecture.toml` and `bmad-create-architecture.user.toml` overrides for the legacy fields (`activation_steps_prepend`, `activation_steps_append`, `persistent_facts`, `on_complete`).
|
||||
1. Resolve customization: `uv run {project-root}/_bmad/scripts/resolve_customization.py --skill {skill-root} --key workflow`. This picks up any `{project-root}/_bmad/custom/bmad-create-architecture.toml` and `bmad-create-architecture.user.toml` overrides for the legacy fields (`activation_steps_prepend`, `activation_steps_append`, `persistent_facts`, `on_complete`).
|
||||
|
||||
2. Load `{project-root}/_bmad/bmm/config.yaml` (and `config.user.yaml` if present) to resolve `{user_name}` and `{communication_language}`.
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ This skill was consolidated into `bmad-prd`. It is retained as a thin compatibil
|
||||
|
||||
## On Activation
|
||||
|
||||
1. Resolve customization: `python3 {project-root}/_bmad/scripts/resolve_customization.py --skill {skill-root} --key workflow`. This picks up any `{project-root}/_bmad/custom/bmad-create-prd.toml` and `bmad-create-prd.user.toml` overrides for the legacy fields (`activation_steps_prepend`, `activation_steps_append`, `persistent_facts`, `on_complete`).
|
||||
1. Resolve customization: `uv run {project-root}/_bmad/scripts/resolve_customization.py --skill {skill-root} --key workflow`. This picks up any `{project-root}/_bmad/custom/bmad-create-prd.toml` and `bmad-create-prd.user.toml` overrides for the legacy fields (`activation_steps_prepend`, `activation_steps_append`, `persistent_facts`, `on_complete`).
|
||||
|
||||
2. Load `{project-root}/_bmad/bmm/config.yaml` (and `config.user.yaml` if present) to resolve `{user_name}` and `{communication_language}`.
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ If you need an explicit user instruction to run them, ask once now for the whole
|
||||
|
||||
### Step 1: Resolve the Workflow Block
|
||||
|
||||
Run: `python3 {project-root}/_bmad/scripts/resolve_customization.py --skill {skill-root} --key workflow`
|
||||
Run: `uv run {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:
|
||||
|
||||
@@ -428,7 +428,7 @@ Activation is complete. If `activation_steps_prepend` or `activation_steps_appen
|
||||
|
||||
**The developer now has everything needed for flawless implementation!**
|
||||
</output>
|
||||
<action>Run: `python3 {project-root}/_bmad/scripts/resolve_customization.py --skill {skill-root} --key workflow.on_complete` — if the resolved value is non-empty, follow it as the final terminal instruction before exiting.</action>
|
||||
<action>Run: `uv run {project-root}/_bmad/scripts/resolve_customization.py --skill {skill-root} --key workflow.on_complete` — if the resolved value is non-empty, follow it as the final terminal instruction before exiting.</action>
|
||||
</step>
|
||||
|
||||
</workflow>
|
||||
|
||||
@@ -27,7 +27,7 @@ description: "Deprecated: `bmad-build` is now the official implementation method
|
||||
|
||||
### Step 1: Resolve the Workflow Block
|
||||
|
||||
Run: `python3 {project-root}/_bmad/scripts/resolve_customization.py --skill {skill-root} --key workflow`
|
||||
Run: `uv run {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:
|
||||
|
||||
@@ -496,7 +496,7 @@ Activation is complete. If `activation_steps_prepend` or `activation_steps_appen
|
||||
<action>Suggest checking {sprint_status} to see project progress</action>
|
||||
</check>
|
||||
<action>Remain flexible - allow user to choose their own path or ask for other assistance</action>
|
||||
<action>Run: `python3 {project-root}/_bmad/scripts/resolve_customization.py --skill {skill-root} --key workflow.on_complete` — if the resolved value is non-empty, follow it as the final terminal instruction before exiting.</action>
|
||||
<action>Run: `uv run {project-root}/_bmad/scripts/resolve_customization.py --skill {skill-root} --key workflow.on_complete` — if the resolved value is non-empty, follow it as the final terminal instruction before exiting.</action>
|
||||
</step>
|
||||
|
||||
</workflow>
|
||||
|
||||
@@ -9,7 +9,7 @@ This skill was consolidated into `bmad-prd`. It is retained as a thin compatibil
|
||||
|
||||
## On Activation
|
||||
|
||||
1. Resolve customization: `python3 {project-root}/_bmad/scripts/resolve_customization.py --skill {skill-root} --key workflow`. This picks up any `{project-root}/_bmad/custom/bmad-edit-prd.toml` and `bmad-edit-prd.user.toml` overrides for the legacy fields (`activation_steps_prepend`, `activation_steps_append`, `persistent_facts`, `on_complete`).
|
||||
1. Resolve customization: `uv run {project-root}/_bmad/scripts/resolve_customization.py --skill {skill-root} --key workflow`. This picks up any `{project-root}/_bmad/custom/bmad-edit-prd.toml` and `bmad-edit-prd.user.toml` overrides for the legacy fields (`activation_steps_prepend`, `activation_steps_append`, `persistent_facts`, `on_complete`).
|
||||
|
||||
2. Load `{project-root}/_bmad/bmm/config.yaml` (and `config.user.yaml` if present) to resolve `{user_name}` and `{communication_language}`.
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ This skill was consolidated into `bmad-prd`. It is retained as a thin compatibil
|
||||
|
||||
## On Activation
|
||||
|
||||
1. Resolve customization: `python3 {project-root}/_bmad/scripts/resolve_customization.py --skill {skill-root} --key workflow`. This picks up any `{project-root}/_bmad/custom/bmad-validate-prd.toml` and `bmad-validate-prd.user.toml` overrides for the legacy fields (`activation_steps_prepend`, `activation_steps_append`, `persistent_facts`, `on_complete`).
|
||||
1. Resolve customization: `uv run {project-root}/_bmad/scripts/resolve_customization.py --skill {skill-root} --key workflow`. This picks up any `{project-root}/_bmad/custom/bmad-validate-prd.toml` and `bmad-validate-prd.user.toml` overrides for the legacy fields (`activation_steps_prepend`, `activation_steps_append`, `persistent_facts`, `on_complete`).
|
||||
|
||||
2. Load `{project-root}/_bmad/bmm/config.yaml` (and `config.user.yaml` if present) to resolve `{user_name}` and `{communication_language}`.
|
||||
|
||||
|
||||
@@ -3368,7 +3368,7 @@ async function runTests() {
|
||||
console.log(`${colors.yellow}Test Suite 46: uv-check version parsing and messaging${colors.reset}\n`);
|
||||
|
||||
try {
|
||||
const { parseUvVersion, detectUv } = require('../tools/installer/core/uv-check');
|
||||
const { parseUvVersion, parsePythonVersion, pythonMeetsMinimum, detectUv, detectPython3 } = require('../tools/installer/core/uv-check');
|
||||
|
||||
// Version parsing
|
||||
const plain = parseUvVersion('uv 0.5.31');
|
||||
@@ -3381,18 +3381,38 @@ async function runTests() {
|
||||
assert(parseUvVersion('command not found: uv') === null, 'non-version output returns null');
|
||||
assert(parseUvVersion(null) === null, 'null output returns null');
|
||||
|
||||
// Detection smoke test — must not throw; result is null or well-formed.
|
||||
// Python version parsing — probed only as the no-uv fallback.
|
||||
const py = parsePythonVersion('Python 3.11.7');
|
||||
assert(py && py.major === 3 && py.minor === 11 && py.patch === 7, 'parses "Python 3.11.7"');
|
||||
assert(parsePythonVersion('Python 3.13.0rc1').raw === '3.13.0', 'parses a prerelease python version');
|
||||
assert(parsePythonVersion('python3: command not found') === null, 'non-version python output returns null');
|
||||
assert(pythonMeetsMinimum({ major: 3, minor: 11 }), '3.11 meets the minimum');
|
||||
assert(pythonMeetsMinimum({ major: 3, minor: 13 }), '3.13 meets the minimum');
|
||||
assert(!pythonMeetsMinimum({ major: 3, minor: 10 }), '3.10 is below the minimum');
|
||||
assert(!pythonMeetsMinimum({ major: 2, minor: 7 }), '2.7 is below the minimum');
|
||||
assert(pythonMeetsMinimum({ major: 4, minor: 0 }), 'a future 4.x meets the minimum');
|
||||
assert(!pythonMeetsMinimum(null), 'a missing python version does not meet the minimum');
|
||||
|
||||
// Detection smoke tests — must not throw; result is null or well-formed.
|
||||
const detectedUv = detectUv();
|
||||
assert(detectedUv === null || typeof detectedUv.version.raw === 'string', 'detectUv returns null or a well-formed result');
|
||||
const detectedPy = detectPython3();
|
||||
assert(detectedPy === null || typeof detectedPy.version.raw === 'string', 'detectPython3 returns null or a well-formed result');
|
||||
|
||||
// checkUvEnvironment branch coverage — stub detection + prompts so the
|
||||
// assertions are deterministic regardless of whether uv is installed.
|
||||
// assertions are deterministic regardless of what's installed locally.
|
||||
const uvCheck = require('../tools/installer/core/uv-check');
|
||||
const promptsModule = require('../tools/installer/prompts');
|
||||
const realUv = { detectUv: uvCheck.detectUv, log: promptsModule.log, note: promptsModule.note };
|
||||
const stubUv = (detectResult) => {
|
||||
const realUv = {
|
||||
detectUv: uvCheck.detectUv,
|
||||
detectPython3: uvCheck.detectPython3,
|
||||
log: promptsModule.log,
|
||||
note: promptsModule.note,
|
||||
};
|
||||
const stubUv = (detectResult, pythonResult = null) => {
|
||||
const seen = { success: [], warn: [], note: [] };
|
||||
uvCheck.detectUv = () => detectResult;
|
||||
uvCheck.detectPython3 = () => pythonResult;
|
||||
promptsModule.log = {
|
||||
success: async (m) => void seen.success.push(m),
|
||||
warn: async (m) => void seen.warn.push(m),
|
||||
@@ -3404,23 +3424,52 @@ async function runTests() {
|
||||
};
|
||||
|
||||
try {
|
||||
// Branch: uv present — success, no warning.
|
||||
// Branch: uv present — success, no warning, and python3 is never probed
|
||||
// (uv provisions its own interpreter from the script's requires-python).
|
||||
let probedPython = false;
|
||||
let seen = stubUv({ version: { major: 0, minor: 5, patch: 31, raw: '0.5.31' } });
|
||||
uvCheck.detectPython3 = () => {
|
||||
probedPython = true;
|
||||
return null;
|
||||
};
|
||||
let result = await uvCheck.checkUvEnvironment();
|
||||
assert(result.status === 'found' && seen.success.length === 1, 'uv present logs success');
|
||||
assert(
|
||||
seen.success[0].includes('Python UV check pass') && seen.warn.length === 0,
|
||||
'uv present shows Python UV check pass, no warning',
|
||||
);
|
||||
assert(!probedPython, 'uv present skips the python3 probe');
|
||||
|
||||
// Branch: uv missing — warn + setup note, never blocks (no prompt).
|
||||
seen = stubUv(null);
|
||||
seen = stubUv(null, { version: { major: 3, minor: 12, patch: 1, raw: '3.12.1' } });
|
||||
result = await uvCheck.checkUvEnvironment();
|
||||
assert(result.status === 'missing' && seen.warn.length === 1, 'uv missing warns');
|
||||
assert(seen.warn[0].includes('de facto standard'), 'uv-missing warning frames uv as the de facto standard');
|
||||
assert(seen.note.length === 1 && seen.note[0].includes('uv'), 'uv missing shows a setup note');
|
||||
assert(seen.warn[0].includes('bmad-build') && seen.warn[0].includes('HALT'), 'uv-missing warning names the skills that halt');
|
||||
assert(!seen.warn[0].includes('de facto standard'), 'uv-missing warning no longer frames uv as optional');
|
||||
assert(seen.warn[0].includes('install itself completes'), 'uv-missing warning says the install still completes');
|
||||
assert(
|
||||
seen.warn[0].includes('python3 3.12.1 is present'),
|
||||
'uv missing with a usable python3 says the direct-interpreter skills still work',
|
||||
);
|
||||
assert(seen.note.length === 1 && seen.note[0] === 'uv required', 'uv missing shows a setup note titled "uv required"');
|
||||
|
||||
// Branch: uv missing and python3 too old — nothing Python-backed runs.
|
||||
seen = stubUv(null, { version: { major: 3, minor: 10, patch: 12, raw: '3.10.12' } });
|
||||
result = await uvCheck.checkUvEnvironment();
|
||||
assert(
|
||||
seen.warn[0].includes('below the required 3.11') && seen.warn[0].includes('no Python-backed skill will run'),
|
||||
'uv missing with python3 < 3.11 reports that nothing Python-backed runs',
|
||||
);
|
||||
assert(result.python.version.raw === '3.10.12', 'the probed python version is returned to the caller');
|
||||
|
||||
// Branch: neither uv nor python3.
|
||||
seen = stubUv(null, null);
|
||||
result = await uvCheck.checkUvEnvironment();
|
||||
assert(seen.warn[0].includes('No python3 on PATH either'), 'uv and python3 both missing is reported');
|
||||
assert(result.python === null, 'no python result is returned when python3 is absent');
|
||||
} finally {
|
||||
uvCheck.detectUv = realUv.detectUv;
|
||||
uvCheck.detectPython3 = realUv.detectPython3;
|
||||
promptsModule.log = realUv.log;
|
||||
promptsModule.note = realUv.note;
|
||||
}
|
||||
|
||||
@@ -1245,9 +1245,23 @@ class Installer {
|
||||
' Get started:',
|
||||
` 1. Launch your AI agent from your project folder`,
|
||||
` 2. Not sure what to do? Invoke the ${color.cyan('bmad-help')} skill and ask it what to do!`,
|
||||
'',
|
||||
` ${color.cyan('Tip:')} BMAD workflows increasingly run Python scripts via ${color.cyan('uv run')} — uv is`,
|
||||
` becoming the de facto standard. If you don't have it yet, ask your agent to set it up.`,
|
||||
);
|
||||
|
||||
// Repeat the uv warning here when it applies. The pre-install probe fires
|
||||
// before every prompt in the run, so by now it is far up the scrollback —
|
||||
// and this box is titled "BMAD is ready to use!", which is only true if
|
||||
// the rendered skills can actually start.
|
||||
const { detectUv } = require('./uv-check');
|
||||
if (!detectUv()) {
|
||||
lines.push(
|
||||
'',
|
||||
` ${color.yellow('⚠ uv is not installed.')} ${color.cyan('bmad-build')} and ${color.cyan('bmad-build-auto')} render through`,
|
||||
` ${color.cyan('uv run')} and will halt on activation until you set it up — ask your agent to`,
|
||||
` "install and set up uv for me", or see https://docs.astral.sh/uv/`,
|
||||
);
|
||||
}
|
||||
|
||||
lines.push(
|
||||
'',
|
||||
` Blog, Docs and Guides: ${color.blue('https://bmadcode.com/')}`,
|
||||
` Community: ${color.blue('https://discord.gg/gk8jAdXWmj')}`,
|
||||
|
||||
@@ -1,14 +1,30 @@
|
||||
const { spawnSync } = require('node:child_process');
|
||||
const prompts = require('../prompts');
|
||||
|
||||
// `uv` (https://docs.astral.sh/uv/) is becoming the de facto standard for
|
||||
// running the Python scripts BMAD workflows shell out to: `uv run <script>`
|
||||
// resolves the interpreter and any dependencies on demand, so skills don't
|
||||
// have to assume a particular `python3` is on PATH. The ecosystem is mid-
|
||||
// migration — some skills still call `python3` directly — so a missing `uv`
|
||||
// is a warning, not a blocker: BMAD installs and runs either way.
|
||||
// `uv` (https://docs.astral.sh/uv/) is how BMAD runs the Python scripts its
|
||||
// skills shell out to: `uv run <script>` resolves the interpreter and any
|
||||
// dependencies on demand from the script's own `requires-python` metadata, so
|
||||
// skills don't have to assume a particular `python3` is on PATH.
|
||||
//
|
||||
// As of v6.11.0 this is a requirement, not a preference: the rendered skills
|
||||
// (`bmad-build`, `bmad-build-auto`) render through `render_skill.py` and HALT
|
||||
// on activation if `uv` is unavailable — there is no interpreter fallback.
|
||||
//
|
||||
// The check still never blocks the install. Core-only, docs-only, and CI
|
||||
// installs are legitimate and never touch a rendered skill, so a missing `uv`
|
||||
// must not fail the run. What it does is tell the truth about what will and
|
||||
// will not work afterwards, here and again in the post-install summary.
|
||||
const RUNTIME_COMMAND = 'uv';
|
||||
|
||||
// Probed only when `uv` is absent. Some skills still invoke the interpreter
|
||||
// directly (`python3 .../resolve_customization.py`), and those scripts declare
|
||||
// `requires-python = ">=3.11"` — `resolve_config.py` hard-exits below it
|
||||
// because `tomllib` is a 3.11 stdlib addition. When `uv` is present it
|
||||
// provisions its own interpreter and whatever `python3` resolves to on PATH is
|
||||
// irrelevant, so probing it then would report a problem that doesn't exist.
|
||||
const PYTHON_COMMAND = 'python3';
|
||||
const MIN_PYTHON = { major: 3, minor: 11 };
|
||||
|
||||
/**
|
||||
* Parse `uv --version` output into version parts.
|
||||
* Example outputs: "uv 0.5.31", "uv 0.5.31 (Homebrew 2025-02-12)".
|
||||
@@ -28,13 +44,44 @@ function parseUvVersion(output) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Probe the local environment for `uv`.
|
||||
* @returns {{version: {major: number, minor: number, patch: number, raw: string}}|null}
|
||||
* Parse `python3 --version` output into version parts.
|
||||
* Example outputs: "Python 3.11.7", "Python 3.13.0rc1".
|
||||
* @param {string} output - stdout/stderr from `python3 --version`
|
||||
* @returns {{major: number, minor: number, patch: number, raw: string}|null}
|
||||
*/
|
||||
function detectUv() {
|
||||
function parsePythonVersion(output) {
|
||||
if (!output) return null;
|
||||
const match = output.match(/Python\s+(\d+)\.(\d+)(?:\.(\d+))?/i);
|
||||
if (!match) return null;
|
||||
return {
|
||||
major: Number(match[1]),
|
||||
minor: Number(match[2]),
|
||||
patch: Number(match[3] || 0),
|
||||
raw: `${match[1]}.${match[2]}.${match[3] || 0}`,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a parsed Python version satisfies the 3.11+ floor the scripts declare.
|
||||
* @param {{major: number, minor: number}|null} version
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function pythonMeetsMinimum(version) {
|
||||
if (!version) return false;
|
||||
if (version.major !== MIN_PYTHON.major) return version.major > MIN_PYTHON.major;
|
||||
return version.minor >= MIN_PYTHON.minor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Run `<command> --version` and hand the combined output to a parser.
|
||||
* @param {string} command
|
||||
* @param {(output: string) => Object|null} parse
|
||||
* @returns {{version: Object}|null}
|
||||
*/
|
||||
function probeVersion(command, parse) {
|
||||
let result;
|
||||
try {
|
||||
result = spawnSync(RUNTIME_COMMAND, ['--version'], {
|
||||
result = spawnSync(command, ['--version'], {
|
||||
encoding: 'utf8',
|
||||
timeout: 5000,
|
||||
windowsHide: true,
|
||||
@@ -43,14 +90,30 @@ function detectUv() {
|
||||
return null;
|
||||
}
|
||||
if (!result || result.error) return null;
|
||||
const version = parseUvVersion(`${result.stdout || ''}\n${result.stderr || ''}`);
|
||||
const version = parse(`${result.stdout || ''}\n${result.stderr || ''}`);
|
||||
return version ? { version } : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Probe the local environment for `uv`.
|
||||
* @returns {{version: {major: number, minor: number, patch: number, raw: string}}|null}
|
||||
*/
|
||||
function detectUv() {
|
||||
return probeVersion(RUNTIME_COMMAND, parseUvVersion);
|
||||
}
|
||||
|
||||
/**
|
||||
* Probe the local environment for `python3`.
|
||||
* @returns {{version: {major: number, minor: number, patch: number, raw: string}}|null}
|
||||
*/
|
||||
function detectPython3() {
|
||||
return probeVersion(PYTHON_COMMAND, parsePythonVersion);
|
||||
}
|
||||
|
||||
function setupHints() {
|
||||
return [
|
||||
'BMAD workflows increasingly run Python scripts via `uv run`, which manages',
|
||||
'the interpreter and dependencies for you — no manual venv or pip needed.',
|
||||
'`uv run` provisions the interpreter and dependencies for you — no manual',
|
||||
'venv or pip, and no need for a particular python3 on PATH.',
|
||||
'',
|
||||
'Easiest path: ask your AI agent to "install and set up uv for me".',
|
||||
'',
|
||||
@@ -62,15 +125,42 @@ function setupHints() {
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* Describe what a missing `uv` leaves working, based on the `python3` fallback.
|
||||
*
|
||||
* With `uv` gone the rendered skills are lost either way; the only question is
|
||||
* whether the skills that invoke the interpreter directly still run, and that
|
||||
* turns on `python3` being 3.11+.
|
||||
*
|
||||
* @param {{version: Object}|null} python - result of detectPython3()
|
||||
* @returns {string}
|
||||
*/
|
||||
function pythonFallbackNote(python) {
|
||||
if (!python) {
|
||||
return 'No python3 on PATH either, so no Python-backed skill will run.';
|
||||
}
|
||||
if (pythonMeetsMinimum(python.version)) {
|
||||
return (
|
||||
`python3 ${python.version.raw} is present, so skills that call the interpreter ` +
|
||||
'directly still work — but the rendered skills above stay unavailable.'
|
||||
);
|
||||
}
|
||||
return (
|
||||
`python3 ${python.version.raw} is present but below the required ` +
|
||||
`${MIN_PYTHON.major}.${MIN_PYTHON.minor}, so no Python-backed skill will run.`
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether `uv` is available and inform the user.
|
||||
*
|
||||
* Warn-don't-block, and no acknowledgement prompt: `uv` is on its way to being
|
||||
* the standard runner for BMAD's Python scripts, but the migration is still in
|
||||
* progress, so the install never stops on its account. The note tells the user
|
||||
* how to set it up (preferably by asking their agent).
|
||||
* Warn-don't-block: a missing `uv` never stops or fails the install, because
|
||||
* core-only and CI installs are legitimate and never render a skill. But the
|
||||
* warning names the concrete consequence — `bmad-build` and `bmad-build-auto`
|
||||
* halt — rather than describing `uv` as a nice-to-have, and `installer.js`
|
||||
* repeats it in the post-install summary so it isn't lost in the scrollback.
|
||||
*
|
||||
* @returns {Promise<{status: 'found'|'missing', detected: Object|null}>}
|
||||
* @returns {Promise<{status: 'found'|'missing', detected: Object|null, python: Object|null}>}
|
||||
*/
|
||||
async function checkUvEnvironment() {
|
||||
// Called via module.exports so tests can stub detection.
|
||||
@@ -78,20 +168,28 @@ async function checkUvEnvironment() {
|
||||
|
||||
if (detected) {
|
||||
await prompts.log.success(`✅ Python UV check pass (uv ${detected.version.raw} detected).`);
|
||||
return { status: 'found', detected };
|
||||
return { status: 'found', detected, python: null };
|
||||
}
|
||||
|
||||
const python = module.exports.detectPython3();
|
||||
|
||||
await prompts.log.warn(
|
||||
"uv not found on PATH. uv is becoming the de facto standard for running BMAD's Python\n" +
|
||||
'scripts (`uv run <script>`), and it provisions the interpreter for you. BMAD installs\n' +
|
||||
'fine without it, but setting up uv now keeps you ahead as workflows adopt it.',
|
||||
'uv not found on PATH. BMAD requires it to run the Python scripts its skills\n' +
|
||||
'shell out to: bmad-build and bmad-build-auto render through `uv run` and will\n' +
|
||||
'HALT on activation without it — there is no interpreter fallback.\n' +
|
||||
`${pythonFallbackNote(python)}\n` +
|
||||
'The install itself completes either way.',
|
||||
);
|
||||
await prompts.note(setupHints(), 'uv recommended');
|
||||
return { status: 'missing', detected: null };
|
||||
await prompts.note(setupHints(), 'uv required');
|
||||
return { status: 'missing', detected: null, python };
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
checkUvEnvironment,
|
||||
detectUv,
|
||||
detectPython3,
|
||||
parseUvVersion,
|
||||
parsePythonVersion,
|
||||
pythonMeetsMinimum,
|
||||
MIN_PYTHON,
|
||||
};
|
||||
|
||||
@@ -12,8 +12,9 @@ startMessage: |
|
||||
🌟 100% free. 100% open source. Always.
|
||||
No paywalls. No gated content. Knowledge shared, not sold.
|
||||
|
||||
🐍 HEADS UP: uv (https://docs.astral.sh/uv/) is becoming the de facto standard
|
||||
for running the Python scripts BMAD workflows rely on (`uv run <script>`).
|
||||
🐍 REQUIRED: uv (https://docs.astral.sh/uv/) runs the Python scripts BMAD
|
||||
skills rely on (`uv run <script>`) and provisions the interpreter itself.
|
||||
Without it, bmad-build and bmad-build-auto halt on activation.
|
||||
If it's not set up yet, ask your AI agent to "install and set up uv for me".
|
||||
|
||||
🌐 CONNECT:
|
||||
|
||||
@@ -204,13 +204,15 @@ class UI {
|
||||
const messageLoader = new MessageLoader();
|
||||
await messageLoader.displayStartMessage();
|
||||
|
||||
// Probe for `uv` before any other prompts: it's becoming the de facto
|
||||
// runner for the Python scripts BMAD workflows shell out to
|
||||
// (`uv run <script>`), and uv provisions the interpreter itself, so it's
|
||||
// the single thing worth checking for. The migration is still in progress
|
||||
// (some skills still call `python3` directly), so this is informational —
|
||||
// warn-don't-block, no ack prompt — and just points the user at setup
|
||||
// (ideally "ask your agent to set up uv"). The installer runs in the
|
||||
// Probe for `uv` before any other prompts: it's the runner for the Python
|
||||
// scripts BMAD skills shell out to (`uv run <script>`), and uv provisions
|
||||
// the interpreter itself, so it's the single thing worth checking for.
|
||||
// As of v6.11.0 `bmad-build` and `bmad-build-auto` HALT without it.
|
||||
//
|
||||
// Still warn-don't-block, with no ack prompt: core-only, docs-only, and
|
||||
// CI installs never touch a rendered skill, so a missing `uv` must not
|
||||
// fail the run. `installer.js` repeats the warning in the post-install
|
||||
// summary so it survives the scrollback. The installer runs in the
|
||||
// destination environment, so probing PATH here tests the right machine.
|
||||
const { checkUvEnvironment } = require('./core/uv-check');
|
||||
await checkUvEnvironment();
|
||||
|
||||
Reference in New Issue
Block a user