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:
Brian
2026-08-09 18:25:48 -05:00
committed by GitHub
parent 328d286d35
commit ade7a966e9
31 changed files with 243 additions and 79 deletions
+3 -3
View File
@@ -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:
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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}`.
+58 -9
View File
@@ -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;
}
+17 -3
View File
@@ -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')}`,
+122 -24
View File
@@ -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,
};
+3 -2
View File
@@ -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:
+9 -7
View File
@@ -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();