diff --git a/CHANGELOG.md b/CHANGELOG.md index 8fee19e99..a6c2e20a0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,46 @@ # Changelog +## v6.9.0 - 2026-06-21 + +### ✨ Headline + +**Reasoning skills get sharper and orchestration gets a memory.** + +**bmad-forge-idea** is a new core skill that takes a half-formed idea and pressure-tests it one Socratic question at a time — with an adversarial attack mode and optional persona rooms — until the idea hardens, proves out, or dies cheaply. + +**bmad-architecture** lands as a ground-up rewrite of the old multi-step create-architecture flow: a lean spine (`ARCHITECTURE-SPINE.md`) that is the source of truth, intent-based routing (Create/Update/Validate), a breadth-coverage rubric so no dimension is silently skipped, and an opt-in reviewer gate. + +**party-mode** is reborn with creatable, savable custom parties, optional party memory, and many pacing and dynamics improvements. + +**Under the hood:** a canonical shared **memlog** (`_bmad/scripts/memlog.py`) replaces per-skill decision logs and is now the standard working-memory primitive across the suite. The installer now checks for **uv** and reframes it as the standard way to run BMAD's Python scripts (`uv run`). Plus an **Astro 6** security upgrade clearing 8+ Dependabot advisories and two new platform targets. + +### ⚠️ Upcoming Breaking Change (in v7) — standardizing on `uv` + +The industry is converging on [**uv**](https://docs.astral.sh/uv/) for running Python, and BMAD is following. Today our skills use a **mix** of `uv run` and direct `python3` invocation. In the **v7 release, every skill that runs a Python script will standardize on `uv run`** instead of calling `python3` directly — `uv` provisions the interpreter and manages dependencies, so scripts run consistently regardless of what's on your PATH. + +**What to do now:** install and set up `uv` ([docs](https://docs.astral.sh/uv/)) — or just ask your AI agent to "install and set up uv for me." Starting this release the installer checks for it and points you to setup if it's missing. `uv` is **not yet required** but without it some skills may have degraded performance or a shim AGENTS.md (or similar) or rule will need to be added to your environment to tell the agent when it sees uv run to use python3 instead. The best course of action though at this time is to install uv. A missing `uv` still warns rather than blocks, but it will be the assumed default in v7. Custom skills and overrides that shell out to `python3` should plan to migrate to `uv run`. + +### 🎁 Features + +* **bmad-forge-idea — new core skill** (#2492). Domain-agnostic idea pressure-testing for the analysis phase: Socratic, one-question-at-a-time interrogation with an adversarial attack mode and optional persona rooms resolved from the installed roster. Hardens or kills an idea cheaply; emits memlog residue and an optional brief that feeds bmad-spec or bmad-quick-dev. Interactive only (menu code FI). +* **bmad-architecture — lean spine rewrite** (#2467, #2475). Replaces the fixed-step `bmad-create-architecture` (retained as a forwarding shim, removed in v7) with intent-based routing across five entry shapes (raw idea, large doc, codebase, feature slice, existing spine). The spine (`ARCHITECTURE-SPINE.md`) is the source of truth and SPEC.md is derived from it. Adds a breadth-coverage rubric (every altitude-owned dimension decided/deferred/open), an opt-in reviewer gate that scales lenses to rigor, and a full non-interactive headless mode. `lint_spine.py` hardened with fence-blanking, robust column detection, and 28 regression tests. +* **party-mode: configurable parties + persistent memory** (#2479, #2484). Custom personas (`party_members`) and named rooms (`party_groups`, with optional scenes), four run modes (auto/session/subagent/agent-team), and a preloaded "Code Review Crew" of five adversarial lenses. Each party keeps append-only session memory under `{memory_dir}//` so sessions resume with prior context; ad-hoc casts stay ephemeral. +* **bmad-brainstorming: facilitation modes + visual composer** (#2445). Three modes (Facilitator / Creative Partner / Ideate for me), append-only memlog with optional `--by` authorship attribution, and a self-contained `brain-selector.html` composer (technique strategy, category chips, filter, copy-to-clipboard, dark mode). Catalog grows to 108 techniques (8 new classics: HMW, JTBD, Empathy Map, Backcasting, TRIZ, Fishbone, Build on What Works, Scenario Cross) plus a convergence phase. +* **Canonical shared memlog script** (#2462). New `src/scripts/memlog.py` — append-only chronological working memory with init/append/set ops, no lifecycle-status design, Python 3.8+ support, 30 tests. Any skill can call it at runtime. +* **Retrospective action items tracked in sprint-status** (#2465). The retrospective step appends an `action_items` section to `sprint-status.yaml`; sprint-status validates and surfaces open items, and sprint-planning preserves them on regenerate. +* **Installer checks for `uv` and reframes it as the standard** (#2495). Replaces the old python3 probe with a `uv` check, adds a heads-up to the install intro and a tip to the "BMAD is ready" summary, and updates docs/script docstrings (en/fr/vi-vn) to frame `uv run` as the standard and `python3` as the transition fallback. Migration-friendly: a missing `uv` warns and points you to setup, never blocks. See the Upcoming Breaking Change note above. +* **New installer platform targets: hermes-agent and CodeWhale** (#2489, #2459). hermes-agent added as a tool target; CodeWhale uses `.codewhale/skills/` (project) and `~/.codewhale/skills/` (global), both with test coverage. + +### 🐛 Fixes + +* **Astro 6 security upgrade clears Dependabot alerts** (#2493). Astro 5.18.1 → 6.4.6 and Starlight 0.37.5 → 0.40.0 (8 XSS/SSRF advisories), esbuild pinned to 0.28.1 (Windows dev-server file read), markdown-it 14.2.0 (smartquotes ReDoS), brace-expansion 5.0.6 (range DoS). Docs content config migrated to `src/content.config.ts`; page output verified identical to baseline. +* **Guard WSL installs from Windows Node** (#2470). Detects and prevents a Windows `node.exe` being used inside WSL, where it would silently fail. +* **Remove empty skill-group dirs after install** (#2461). Prunes empty parent dirs (e.g. `_bmad/bmm/1-analysis`) left after skill cleanup, with a path-boundary check to avoid sibling-dir collisions. +* **bmad-create-epics-and-stories discovers bmad-ux spine outputs** (#2446). Prerequisites now recognize `DESIGN.md` / `EXPERIENCE.md` alongside the legacy `ux-spec.md`. +* **Pass diff inline to the blind-hunter reviewer** (#2463). Diff output is passed inline in the subagent prompt rather than via a file the reviewer can't read, preventing context-starved hallucination. +* **Website: nav height for dual announcement banners** (#2473). Fixes layout crowding when two banners show at once. +* **Workflow clarity & numbering** — clarify quick-dev subagent use across code-review/create-story/quick-dev (#2450), renumber retrospective steps (#2448). + ## v6.8.0 - 2026-05-25 ### ✨ Headline diff --git a/docs/fr/how-to/customize-bmad.md b/docs/fr/how-to/customize-bmad.md index 6cb2cc54d..45382b817 100644 --- a/docs/fr/how-to/customize-bmad.md +++ b/docs/fr/how-to/customize-bmad.md @@ -22,7 +22,7 @@ Le skill `bmad-customize` est un assistant de rédaction guidée pour les **opti :::note[Prérequis] - BMad installé dans votre projet (voir [Comment installer BMad](./install-bmad.md)) -- Python 3.11+ sur votre PATH (pour le script de résolution — utilise `tomllib` de la bibliothèque standard, pas de `pip install`, pas de `uv`, pas de virtualenv) +- Un moyen d’exécuter le script de résolution — BMad adopte `uv` comme standard (`uv run`, qui provisionne Python pour vous) ; un simple `python3` 3.11+ sur votre PATH fonctionne toujours pendant la transition. Le script n’utilise que `tomllib` de la bibliothèque standard, il n’y a donc rien à `pip install`. - Un éditeur de texte pour les fichiers TOML ::: @@ -201,15 +201,15 @@ persistent_facts = [ ## Comment fonctionne la résolution -À l’activation, le SKILL.md de l’agent exécute un script Python partagé qui effectue la fusion à trois couches et renvoie le bloc résolu en JSON. Le script utilise le module `tomllib` de la bibliothèque standard Python (aucune dépendance externe), donc `python3` suffit : +À l’activation, le SKILL.md de l’agent exécute un script Python partagé qui effectue la fusion à trois couches et renvoie le bloc résolu en JSON. Le script utilise uniquement le module `tomllib` de la bibliothèque standard Python (aucune dépendance externe). BMad adopte `uv run` comme standard pour exécuter ces scripts (uv provisionne un Python adapté pour vous) ; un simple `python3` fonctionne toujours pendant la transition : ```bash -python3 {project-root}/_bmad/scripts/resolve_customization.py \ +uv run {project-root}/_bmad/scripts/resolve_customization.py \ --skill {skill-root} \ --key agent ``` -**Prérequis** : Python 3.11+ (les versions antérieures n’incluent pas `tomllib`). Pas de `pip install`, pas de `uv`, pas de virtualenv. Vérifiez avec `python3 --version`. Certaines plateformes (macOS sans Homebrew, Ubuntu 22.04) ont `python3` par défaut en 3.10 ou antérieur, vous devrez peut-être installer 3.11+ séparément. +**Prérequis** : Python 3.11+ (les versions antérieures n’incluent pas `tomllib`). Rien à `pip install`. L’exécution via `uv run` est le standard à venir — uv résout un interpréteur adapté pour vous. Si vous l’exécutez directement avec `python3` pendant la transition, vérifiez votre version avec `python3 --version` ; certaines plateformes (macOS sans Homebrew, Ubuntu 22.04) ont `python3` par défaut en 3.10 ou antérieur, vous devrez peut-être installer 3.11+ séparément. `--skill` pointe vers le répertoire installé du skill (où se trouve `customize.toml`). Le nom du skill est déduit du basename du répertoire, et le script cherche automatiquement `_bmad/custom/{skill-name}.toml` et `{skill-name}.user.toml`. @@ -217,17 +217,17 @@ Exemples d’utilisation : ```bash # Résoudre le bloc agent complet -python3 {project-root}/_bmad/scripts/resolve_customization.py \ +uv run {project-root}/_bmad/scripts/resolve_customization.py \ --skill /chemin/absolu/vers/bmad-agent-pm \ --key agent # Résoudre un seul champ -python3 {project-root}/_bmad/scripts/resolve_customization.py \ +uv run {project-root}/_bmad/scripts/resolve_customization.py \ --skill /chemin/absolu/vers/bmad-agent-pm \ --key agent.icon # Dump complet -python3 {project-root}/_bmad/scripts/resolve_customization.py \ +uv run {project-root}/_bmad/scripts/resolve_customization.py \ --skill /chemin/absolu/vers/bmad-agent-pm ``` diff --git a/docs/how-to/customize-bmad.md b/docs/how-to/customize-bmad.md index 9433a8820..0458607a0 100644 --- a/docs/how-to/customize-bmad.md +++ b/docs/how-to/customize-bmad.md @@ -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)) -- Python 3.11+ on your PATH (for the resolver script -- uses stdlib `tomllib`, no `pip install`, no `uv`, no virtualenv) +- 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`. - A text editor for TOML files ::: @@ -201,15 +201,15 @@ 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 the Python standard library's `tomllib` module (no external dependencies), so plain `python3` is enough: +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: ```bash -python3 {project-root}/_bmad/scripts/resolve_customization.py \ +uv run {project-root}/_bmad/scripts/resolve_customization.py \ --skill {skill-root} \ --key agent ``` -**Requirements**: Python 3.11+ (earlier versions don't include `tomllib`). No `pip install`, no `uv`, no virtualenv. Check 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**: 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. `--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. @@ -217,17 +217,17 @@ Useful invocations: ```bash # Resolve the full agent block -python3 {project-root}/_bmad/scripts/resolve_customization.py \ +uv run {project-root}/_bmad/scripts/resolve_customization.py \ --skill /abs/path/to/bmad-agent-pm \ --key agent # Resolve a single field -python3 {project-root}/_bmad/scripts/resolve_customization.py \ +uv run {project-root}/_bmad/scripts/resolve_customization.py \ --skill /abs/path/to/bmad-agent-pm \ --key agent.icon # Full dump -python3 {project-root}/_bmad/scripts/resolve_customization.py \ +uv run {project-root}/_bmad/scripts/resolve_customization.py \ --skill /abs/path/to/bmad-agent-pm ``` diff --git a/docs/vi-vn/how-to/customize-bmad.md b/docs/vi-vn/how-to/customize-bmad.md index eecc14728..6ac065e17 100644 --- a/docs/vi-vn/how-to/customize-bmad.md +++ b/docs/vi-vn/how-to/customize-bmad.md @@ -22,7 +22,7 @@ Skill `bmad-customize` là trợ lý tạo cấu hình có hướng dẫn cho ** :::note[Điều kiện tiên quyết] - BMad đã được cài trong dự án của bạn (xem [Cách cài đặt BMad](./install-bmad.md)) -- Python 3.11+ có trên PATH của bạn (để chạy resolver; dùng stdlib `tomllib`, không cần `pip install`, `uv` hay virtualenv) +- Một cách để chạy resolver script — BMad đang chuẩn hóa sang `uv` (`uv run`, tự cấp Python cho bạn); một `python3` 3.11+ thuần trên PATH vẫn dùng được trong giai đoạn chuyển đổi. Script chỉ dùng stdlib `tomllib`, nên không cần `pip install` gì cả. - Một trình soạn thảo văn bản cho file TOML ::: @@ -201,15 +201,15 @@ persistent_facts = [ ## Cách quá trình resolve diễn ra -Khi agent được kích hoạt, `SKILL.md` của nó sẽ gọi một shared Python script để merge ba lớp nói trên và trả về block kết quả ở dạng JSON. Script này dùng `tomllib` của Python stdlib, nên `python3` thuần là đủ: +Khi agent được kích hoạt, `SKILL.md` của nó sẽ gọi một shared Python script để merge ba lớp nói trên và trả về block kết quả ở dạng JSON. Script này chỉ dùng `tomllib` của Python stdlib (không có dependency ngoài). BMad đang chuẩn hóa sang `uv run` để chạy các script này (uv tự cấp một bản Python phù hợp cho bạn); một `python3` thuần vẫn dùng được trong giai đoạn chuyển đổi: ```bash -python3 {project-root}/_bmad/scripts/resolve_customization.py \ +uv run {project-root}/_bmad/scripts/resolve_customization.py \ --skill {skill-root} \ --key agent ``` -**Yêu cầu**: Python 3.11+ vì các phiên bản cũ hơn không có `tomllib`. Không cần `pip install`, không cần `uv`, không cần virtualenv. Bạn có thể kiểm tra bằng `python3 --version`. Trên một số nền tảng, `python3` mặc định vẫn là 3.10 hoặc thấp hơn, nên có thể bạn sẽ phải cài 3.11+ riêng. +**Yêu cầu**: Python 3.11+ vì các phiên bản cũ hơn không có `tomllib`; không cần `pip install` gì. Chạy qua `uv run` là chuẩn về sau — uv tự tìm một bản interpreter phù hợp cho bạn. Nếu bạn chạy trực tiếp bằng `python3` trong giai đoạn chuyển đổi, hãy kiểm tra phiên bản bằng `python3 --version`: trên một số nền tảng, `python3` mặc định vẫn là 3.10 hoặc thấp hơn, nên có thể bạn sẽ phải cài 3.11+ riêng. `--skill` trỏ vào thư mục skill đã cài, nơi có file `customize.toml`. Tên skill được lấy từ basename của thư mục, sau đó script sẽ tự tìm `_bmad/custom/{skill-name}.toml` và `{skill-name}.user.toml`. @@ -217,17 +217,17 @@ Một số lệnh hữu ích: ```bash # Resolve toàn bộ block agent -python3 {project-root}/_bmad/scripts/resolve_customization.py \ +uv run {project-root}/_bmad/scripts/resolve_customization.py \ --skill /duong-dan/tuyet-doi/toi/bmad-agent-pm \ --key agent # Resolve một trường cụ thể -python3 {project-root}/_bmad/scripts/resolve_customization.py \ +uv run {project-root}/_bmad/scripts/resolve_customization.py \ --skill /duong-dan/tuyet-doi/toi/bmad-agent-pm \ --key agent.icon # Dump toàn bộ -python3 {project-root}/_bmad/scripts/resolve_customization.py \ +uv run {project-root}/_bmad/scripts/resolve_customization.py \ --skill /duong-dan/tuyet-doi/toi/bmad-agent-pm ``` diff --git a/package-lock.json b/package-lock.json index c30f26b3e..394cf426f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "bmad-method", - "version": "6.8.0", + "version": "6.9.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "bmad-method", - "version": "6.8.0", + "version": "6.9.0", "license": "MIT", "dependencies": { "@clack/core": "^1.3.1", diff --git a/package.json b/package.json index 293bf401f..7506476d3 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "bmad-method", - "version": "6.8.0", + "version": "6.9.0", "description": "Breakthrough Method of Agile AI-driven Development", "keywords": [ "agile", diff --git a/src/core-skills/bmad-brainstorming/scripts/tests/test_brain.py b/src/core-skills/bmad-brainstorming/scripts/tests/test_brain.py index 5da6955a6..951bd4e77 100644 --- a/src/core-skills/bmad-brainstorming/scripts/tests/test_brain.py +++ b/src/core-skills/bmad-brainstorming/scripts/tests/test_brain.py @@ -207,11 +207,11 @@ def test_unknown_category_style_uses_fallback_glyph(): def test_shipped_selector_is_in_sync_with_catalog(): # foolproofing: if someone edits brain-methods.csv they must regenerate the page. - # Regenerate with: python3 brain.py html --out assets/brain-selector.html + # Regenerate with: uv run brain.py html --out assets/brain-selector.html asset = brain.DEFAULT_FILE.parent / "brain-selector.html" assert asset.is_file(), "missing assets/brain-selector.html — generate it" expected = brain.html_doc(brain.load(brain.DEFAULT_FILE)) assert asset.read_text(encoding="utf-8") == expected, ( "assets/brain-selector.html is stale; regenerate: " - "python3 brain.py html --out assets/brain-selector.html" + "uv run brain.py html --out assets/brain-selector.html" ) diff --git a/src/core-skills/bmad-customize/scripts/tests/test_list_customizable_skills.py b/src/core-skills/bmad-customize/scripts/tests/test_list_customizable_skills.py index a7be22ece..916b7c3b9 100644 --- a/src/core-skills/bmad-customize/scripts/tests/test_list_customizable_skills.py +++ b/src/core-skills/bmad-customize/scripts/tests/test_list_customizable_skills.py @@ -13,7 +13,7 @@ Exercises the scanner against a synthesized install tree: - malformed TOML (surfaces as an error without aborting) - multiple skills roots (e.g. project-local + user-global mix) -Run: python3 scripts/tests/test_list_customizable_skills.py +Run: uv run scripts/tests/test_list_customizable_skills.py """ from __future__ import annotations diff --git a/src/scripts/resolve_config.py b/src/scripts/resolve_config.py index eb9e20288..fd762e778 100644 --- a/src/scripts/resolve_config.py +++ b/src/scripts/resolve_config.py @@ -10,12 +10,14 @@ Reads from four layers (highest priority last): Outputs merged JSON to stdout. Errors go to stderr. -Requires Python 3.11+ (uses stdlib `tomllib`). No `uv`, no `pip install`, -no virtualenv — plain `python3` is sufficient. +Uses only the Python stdlib (`tomllib`) — no third-party dependencies. +BMad is standardizing on `uv run` to invoke scripts (uv provisions a suitable +interpreter for you); a plain `python3` on PATH still works during the +transition. Either runner needs Python 3.11+ for `tomllib`. - python3 resolve_config.py --project-root /abs/path/to/project - python3 resolve_config.py --project-root ... --key core - python3 resolve_config.py --project-root ... --key agents + uv run resolve_config.py --project-root /abs/path/to/project + uv run resolve_config.py --project-root ... --key core + uv run resolve_config.py --project-root ... --key agents Merge rules (same as resolve_customization.py): - Scalars: override wins diff --git a/src/scripts/resolve_customization.py b/src/scripts/resolve_customization.py index 3299e1ade..5e7430ca1 100755 --- a/src/scripts/resolve_customization.py +++ b/src/scripts/resolve_customization.py @@ -11,12 +11,14 @@ Skill name is derived from the basename of the skill directory. Outputs merged JSON to stdout. Errors go to stderr. -Requires Python 3.11+ (uses stdlib `tomllib`). No `uv`, no `pip install`, -no virtualenv — plain `python3` is sufficient. +Uses only the Python stdlib (`tomllib`) — no third-party dependencies. +BMad is standardizing on `uv run` to invoke scripts (uv provisions a suitable +interpreter for you); a plain `python3` on PATH still works during the +transition. Either runner needs Python 3.11+ for `tomllib`. - python3 resolve_customization.py --skill /abs/path/to/skill-dir - python3 resolve_customization.py --skill ... --key agent - python3 resolve_customization.py --skill ... --key agent.menu + uv run resolve_customization.py --skill /abs/path/to/skill-dir + uv run resolve_customization.py --skill ... --key agent + uv run resolve_customization.py --skill ... --key agent.menu Merge rules (purely structural — no field-name special-casing): - Scalars (string, int, bool, float): override wins diff --git a/test/test-installation-components.js b/test/test-installation-components.js index f511b4376..b1acea6e6 100644 --- a/test/test-installation-components.js +++ b/test/test-installation-components.js @@ -3319,134 +3319,63 @@ async function runTests() { console.log(''); // ============================================================ - // Test Suite 46: Python environment check (version parsing + classification) + // Test Suite 46: uv environment check (version parsing + messaging) // ============================================================ - console.log(`${colors.yellow}Test Suite 46: python-check version parsing and classification${colors.reset}\n`); + console.log(`${colors.yellow}Test Suite 46: uv-check version parsing and messaging${colors.reset}\n`); try { - const { parsePythonVersion, classifyPython, detectPython } = require('../tools/installer/core/python-check'); + const { parseUvVersion, detectUv } = require('../tools/installer/core/uv-check'); // Version parsing - const v312 = parsePythonVersion('Python 3.12.1'); - assert(v312 && v312.major === 3 && v312.minor === 12 && v312.patch === 1, 'parses "Python 3.12.1"'); - const v311 = parsePythonVersion('Python 3.11.0\n'); - assert(v311 && v311.raw === '3.11.0', 'parses with trailing newline'); - const v2 = parsePythonVersion('\nPython 2.7.18'); - assert(v2 && v2.major === 2, 'parses Python 2 output (stderr-style)'); - const noPatch = parsePythonVersion('Python 3.13'); + const plain = parseUvVersion('uv 0.5.31'); + assert(plain && plain.major === 0 && plain.minor === 5 && plain.patch === 31, 'parses "uv 0.5.31"'); + const brew = parseUvVersion('uv 0.5.31 (Homebrew 2025-02-12)'); + assert(brew && brew.raw === '0.5.31', 'parses uv version with build suffix'); + const noPatch = parseUvVersion('uv 1.2'); assert(noPatch && noPatch.patch === 0, 'missing patch defaults to 0'); - assert(parsePythonVersion('') === null, 'empty output returns null'); - assert(parsePythonVersion('command not found: python3') === null, 'non-version output returns null'); - assert(parsePythonVersion(null) === null, 'null output returns null'); + assert(parseUvVersion('') === null, 'empty output returns null'); + assert(parseUvVersion('command not found: uv') === null, 'non-version output returns null'); + assert(parseUvVersion(null) === null, 'null output returns null'); - // Classification against feature requirements - assert(classifyPython({ major: 3, minor: 11 }) === 'full', '3.11 is full support (tomllib floor)'); - assert(classifyPython({ major: 3, minor: 13 }) === 'full', '3.13 is full support'); - assert(classifyPython({ major: 4, minor: 0 }) === 'full', 'hypothetical 4.0 is full support'); - assert(classifyPython({ major: 3, minor: 10 }) === 'partial', '3.10 is partial (memlog yes, tomllib no)'); - assert(classifyPython({ major: 3, minor: 8 }) === 'partial', '3.8 is partial (memlog floor)'); - assert(classifyPython({ major: 3, minor: 7 }) === 'unsupported', '3.7 is unsupported'); - assert(classifyPython({ major: 2, minor: 7 }) === 'unsupported', '2.7 is unsupported'); - assert(classifyPython(null) === 'none', 'no python is none'); + // Detection smoke test — 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'); - // Detection smoke test — must not throw, and if it finds a Python the - // result must be well-formed. (CI machines may or may not have Python.) - const detected = detectPython(); - assert( - detected === null || - (typeof detected.command === 'string' && - typeof detected.version.raw === 'string' && - typeof detected.isRuntimeCommand === 'boolean'), - 'detectPython returns null or a well-formed result', - ); - - // checkPythonEnvironment branch coverage — stub detection, prompts, and - // process.exit so the assertions are deterministic regardless of the - // machine's Python. python-check resolves detectPython via module.exports - // and prompts via the shared module object, so swapping properties works. - const pythonCheck = require('../tools/installer/core/python-check'); + // checkUvEnvironment branch coverage — stub detection + prompts so the + // assertions are deterministic regardless of whether uv is installed. + const uvCheck = require('../tools/installer/core/uv-check'); const promptsModule = require('../tools/installer/prompts'); - const real = { - detectPython: pythonCheck.detectPython, - log: promptsModule.log, - note: promptsModule.note, - select: promptsModule.select, - cancel: promptsModule.cancel, - exit: process.exit, - }; - const stub = (detectResult, selectAnswer) => { - const seen = { success: [], warn: [], info: [], note: [], select: [], cancel: [], exit: [] }; - pythonCheck.detectPython = () => detectResult; + const realUv = { detectUv: uvCheck.detectUv, log: promptsModule.log, note: promptsModule.note }; + const stubUv = (detectResult) => { + const seen = { success: [], warn: [], note: [] }; + uvCheck.detectUv = () => detectResult; promptsModule.log = { success: async (m) => void seen.success.push(m), warn: async (m) => void seen.warn.push(m), - info: async (m) => void seen.info.push(m), + info: async () => {}, error: async () => {}, }; promptsModule.note = async (m, t) => void seen.note.push(t || m); - promptsModule.select = async (opts) => { - seen.select.push(opts.message); - return selectAnswer; - }; - promptsModule.cancel = async (m) => void seen.cancel.push(m); - process.exit = (code) => { - seen.exit.push(code); - throw new Error('__stub_exit__'); - }; return seen; }; try { - const v = (major, minor, patch) => ({ major, minor, patch, raw: `${major}.${minor}.${patch}` }); + // Branch: uv present — success, no warning. + let seen = stubUv({ version: { major: 0, minor: 5, patch: 31, raw: '0.5.31' } }); + let result = await uvCheck.checkUvEnvironment(); + assert(result.status === 'found' && seen.success.length === 1, 'uv present logs success'); + assert(seen.success[0].includes('uv run') && seen.warn.length === 0, 'uv present mentions uv run, no warning'); - // Branch: full support via the runtime command — success, no prompt. - let seen = stub({ command: 'python3', version: v(3, 12, 1), isRuntimeCommand: true }, 'continue'); - let result = await pythonCheck.checkPythonEnvironment(); - assert(result.status === 'full' && seen.success.length === 1, 'full support via python3 logs success'); - assert(seen.select.length === 0 && seen.warn.length === 0, 'full support via python3 skips warning and ack prompt'); - - // Branch: modern Python found, but not as `python3` — runtime mismatch. - seen = stub({ command: 'py -3', version: v(3, 12, 0), isRuntimeCommand: false }, 'continue'); - result = await pythonCheck.checkPythonEnvironment(); - assert(seen.success.length === 0, 'python3-mismatch never reports full support'); - assert( - seen.warn.length === 1 && seen.warn[0].includes('python3') && seen.warn[0].includes('py -3'), - 'python3-mismatch warns that scripts invoke python3', - ); - assert(seen.select.length === 1 && result.status === 'full', 'python3-mismatch still requires the ack prompt'); - - // Branch: partial support (3.8–3.10) — warn + ack, continue returns. - seen = stub({ command: 'python3', version: v(3, 9, 5), isRuntimeCommand: true }, 'continue'); - result = await pythonCheck.checkPythonEnvironment(); - assert( - result.status === 'partial' && seen.warn.length === 1 && seen.warn[0].includes('3.11+'), - 'partial support warns about tomllib floor', - ); - assert(seen.select.length === 1 && seen.exit.length === 0, 'partial support prompts and continue proceeds'); - - // Branch: no Python, non-interactive — warn + info, never prompts. - seen = stub(null, 'continue'); - result = await pythonCheck.checkPythonEnvironment({ nonInteractive: true }); - assert(result.status === 'none' && seen.warn[0].includes('No Python found'), 'non-interactive with no Python warns'); - assert(seen.select.length === 0 && seen.info.length === 1, 'non-interactive skips the ack prompt and logs continuation'); - - // Branch: no Python, interactive, user quits — cancel message + exit 0. - seen = stub(null, 'quit'); - let threw = false; - try { - await pythonCheck.checkPythonEnvironment(); - } catch (error) { - threw = error.message === '__stub_exit__'; - } - assert(threw && seen.exit.length === 1 && seen.exit[0] === 0, 'quit choice exits 0 (user-cancel convention)'); - assert(seen.cancel.length === 1, 'quit choice shows the cancel guidance'); + // Branch: uv missing — warn + setup note, never blocks (no prompt). + seen = stubUv(null); + 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'); } finally { - pythonCheck.detectPython = real.detectPython; - promptsModule.log = real.log; - promptsModule.note = real.note; - promptsModule.select = real.select; - promptsModule.cancel = real.cancel; - process.exit = real.exit; + uvCheck.detectUv = realUv.detectUv; + promptsModule.log = realUv.log; + promptsModule.note = realUv.note; } } catch (error) { console.log(`${colors.red}Test Suite 46 setup failed: ${error.message}${colors.reset}`); diff --git a/tools/installer/core/installer.js b/tools/installer/core/installer.js index 9c6c6cb6c..61c3408de 100644 --- a/tools/installer/core/installer.js +++ b/tools/installer/core/installer.js @@ -1233,6 +1233,9 @@ class Installer { ` 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.`, + '', ` Blog, Docs and Guides: ${color.blue('https://bmadcode.com/')}`, ` Community: ${color.blue('https://discord.gg/gk8jAdXWmj')}`, ); diff --git a/tools/installer/core/python-check.js b/tools/installer/core/python-check.js deleted file mode 100644 index d8539cac3..000000000 --- a/tools/installer/core/python-check.js +++ /dev/null @@ -1,199 +0,0 @@ -const { spawnSync } = require('node:child_process'); -const prompts = require('../prompts'); - -// Python 3.11 added stdlib `tomllib` (PEP 680), which the shared scripts in -// src/scripts/ (resolve_config.py, resolve_customization.py) require to read -// BMAD's TOML config files. memlog.py is more lenient and runs on 3.8+. -const PYTHON_FULL_SUPPORT = { major: 3, minor: 11 }; -const PYTHON_PARTIAL_SUPPORT = { major: 3, minor: 8 }; - -// Every runtime call site (skill steps, on_complete hooks) invokes a literal -// `python3`, so only that command's version vouches for BMAD features. The -// fallback probes exist to tell the user "Python is installed, but not under -// the name BMAD uses" instead of a misleading "No Python found". -const RUNTIME_COMMAND = 'python3'; -const PROBE_CANDIDATES = - process.platform === 'win32' - ? [ - { command: 'python3', args: ['--version'] }, - { command: 'py', args: ['-3', '--version'] }, - { command: 'python', args: ['--version'] }, - ] - : [ - { command: 'python3', args: ['--version'] }, - { command: 'python', args: ['--version'] }, - ]; - -/** - * Parse a `python --version` output line into version parts. - * Python 3 prints to stdout; Python 2 printed to stderr — callers pass both. - * @param {string} output - Combined stdout/stderr from `python --version` - * @returns {{major: number, minor: number, patch: number, raw: string}|null} - */ -function parsePythonVersion(output) { - if (!output) return null; - const match = output.match(/Python\s+(\d+)\.(\d+)(?:\.(\d+))?/); - 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}`, - }; -} - -/** - * Classify a detected Python version against BMAD's feature requirements. - * @param {{major: number, minor: number}|null} version - * @returns {'full'|'partial'|'unsupported'|'none'} - */ -function classifyPython(version) { - if (!version) return 'none'; - const { major, minor } = version; - if (major > PYTHON_FULL_SUPPORT.major || (major === PYTHON_FULL_SUPPORT.major && minor >= PYTHON_FULL_SUPPORT.minor)) { - return 'full'; - } - if (major === PYTHON_PARTIAL_SUPPORT.major && minor >= PYTHON_PARTIAL_SUPPORT.minor) { - return 'partial'; - } - return 'unsupported'; -} - -/** - * Run one probe candidate and return its parsed version, or null. - * @param {{command: string, args: string[]}} candidate - * @returns {{major: number, minor: number, patch: number, raw: string}|null} - */ -function probeVersion(candidate) { - const run = (extra = {}) => - spawnSync(candidate.command, candidate.args, { - encoding: 'utf8', - timeout: 5000, - windowsHide: true, - ...extra, - }); - let result = run(); - // Node >=18.20/20.12 refuses to spawn .bat/.cmd without a shell - // (CVE-2024-27980 hardening) and reports EINVAL — pyenv-win ships its - // python shims as .bat. Args here are static literals, so a shell retry - // is injection-safe. - if (result.error && result.error.code === 'EINVAL' && process.platform === 'win32') { - result = run({ shell: true }); - } - if (result.error) return null; - return parsePythonVersion(`${result.stdout || ''}\n${result.stderr || ''}`); -} - -/** - * Probe the local environment for a Python interpreter. - * Tries each candidate command and returns the first that reports a version. - * `isRuntimeCommand` is true only when the match is `python3` — the command - * BMAD scripts actually invoke. - * @returns {{command: string, version: {major: number, minor: number, patch: number, raw: string}, isRuntimeCommand: boolean}|null} - */ -function detectPython() { - for (const candidate of PROBE_CANDIDATES) { - try { - const version = probeVersion(candidate); - if (version) { - const display = candidate.args.length > 1 ? `${candidate.command} ${candidate.args.slice(0, -1).join(' ')}` : candidate.command; - return { command: display, version, isRuntimeCommand: candidate.command === RUNTIME_COMMAND }; - } - } catch { - // Candidate not runnable — try the next one. - } - } - return null; -} - -function upgradeHints() { - return [ - 'How to get Python 3.11+ (as `python3`):', - ' macOS: brew install python3', - ' Windows: winget install Python.Python.3.12 (then ensure `python3` resolves, e.g. enable the python3 alias)', - ' Linux/WSL: sudo apt install python3 (Ubuntu 24.04+ ships 3.12; older distros: use pyenv or deadsnakes)', - ' Docker: add python3 to your image (e.g. apk add python3 / apt-get install -y python3)', - ].join('\n'); -} - -/** - * Check the local Python environment and warn about degraded BMAD features. - * - * Warn-don't-block: most of BMAD works without Python, so the install always - * may proceed — but the user must explicitly acknowledge the warning so it - * can't scroll past unseen. In non-interactive runs (--yes, or stdin is not - * a TTY) the warning is logged and the install continues without a prompt. - * - * @param {Object} [options] - * @param {boolean} [options.nonInteractive=false] - Skip the ack prompt (--yes, or no TTY) - * @returns {Promise<{status: string, detected: Object|null}>} - */ -async function checkPythonEnvironment({ nonInteractive = false } = {}) { - // Called via module.exports so tests can stub detection. - const detected = module.exports.detectPython(); - const status = classifyPython(detected ? detected.version : null); - - if (status === 'full' && detected.isRuntimeCommand) { - await prompts.log.success(`Python ${detected.version.raw} detected (${detected.command}) — all BMAD features supported.`); - return { status, detected }; - } - - if (detected && !detected.isRuntimeCommand) { - await prompts.log.warn( - `Python ${detected.version.raw} found via \`${detected.command}\`, but BMAD scripts invoke \`python3\`, which is not on PATH.\n` + - `Python-powered features (memlog session memory, TOML config resolution) won't run until \`python3\` resolves —\n` + - `add a python3 alias/shim, or reinstall Python with the python3 launcher enabled.`, - ); - } else if (status === 'partial') { - await prompts.log.warn( - `Python ${detected.version.raw} detected (${detected.command}) — BMAD's TOML config tools need Python 3.11+ (stdlib tomllib).\n` + - `Works: memlog session memory. Won't work: config/customization resolution scripts.`, - ); - } else { - const found = - status === 'unsupported' ? `Python ${detected.version.raw} detected (${detected.command}) — too old.` : 'No Python found on PATH.'; - await prompts.log.warn( - `${found} BMAD installs fine without it, but Python-powered features\n` + - `(memlog session memory, TOML config resolution) won't run until Python 3.11+ is available.`, - ); - } - await prompts.note(upgradeHints(), 'Python 3.11+ recommended'); - - if (nonInteractive) { - await prompts.log.info('Continuing anyway (non-interactive run). You can fix Python later — no reinstall needed.'); - return { status, detected }; - } - - const choice = await prompts.select({ - message: "BMAD's Python-powered features won't work yet. How do you want to proceed?", - choices: [ - { - name: 'Continue install', - value: 'continue', - hint: 'BMAD works without Python — you can fix Python later, no reinstall needed', - }, - { - name: 'Quit and fix Python first', - value: 'quit', - hint: 'make Python 3.11+ available as python3, then re-run the installer', - }, - ], - default: 'continue', - }); - - if (choice === 'quit') { - await prompts.cancel('Make Python 3.11+ available as `python3` (see hints above), then re-run the installer.'); - process.exit(0); - } - - return { status, detected }; -} - -module.exports = { - checkPythonEnvironment, - detectPython, - parsePythonVersion, - classifyPython, - PYTHON_FULL_SUPPORT, - PYTHON_PARTIAL_SUPPORT, -}; diff --git a/tools/installer/core/uv-check.js b/tools/installer/core/uv-check.js new file mode 100644 index 000000000..cddb096fd --- /dev/null +++ b/tools/installer/core/uv-check.js @@ -0,0 +1,97 @@ +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