bmad-method 6.8.1-next.2 → 6.8.1-next.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. package/.claude-plugin/marketplace.json +9 -3
  2. package/package.json +10 -4
  3. package/src/bmm-skills/1-analysis/bmad-prfaq/SKILL.md +2 -2
  4. package/src/bmm-skills/1-analysis/bmad-product-brief/SKILL.md +8 -8
  5. package/src/bmm-skills/2-plan-workflows/bmad-prd/SKILL.md +7 -7
  6. package/src/bmm-skills/2-plan-workflows/bmad-prd/assets/headless-schemas.md +2 -2
  7. package/src/bmm-skills/2-plan-workflows/bmad-prd/customize.toml +1 -1
  8. package/src/bmm-skills/2-plan-workflows/bmad-prd/references/headless.md +1 -1
  9. package/src/bmm-skills/2-plan-workflows/bmad-prd/references/validate.md +1 -1
  10. package/src/bmm-skills/2-plan-workflows/bmad-ux/SKILL.md +8 -8
  11. package/src/bmm-skills/2-plan-workflows/bmad-ux/assets/design-directions.md +1 -1
  12. package/src/bmm-skills/2-plan-workflows/bmad-ux/assets/headless-schemas.md +2 -2
  13. package/src/bmm-skills/2-plan-workflows/bmad-ux/assets/key-screens.md +4 -4
  14. package/src/bmm-skills/2-plan-workflows/bmad-ux/customize.toml +1 -1
  15. package/src/bmm-skills/2-plan-workflows/bmad-ux/references/creative-tools.md +1 -1
  16. package/src/bmm-skills/2-plan-workflows/bmad-ux/references/headless.md +1 -1
  17. package/src/bmm-skills/2-plan-workflows/bmad-ux/references/validate.md +2 -2
  18. package/src/bmm-skills/3-solutioning/bmad-agent-architect/customize.toml +2 -2
  19. package/src/bmm-skills/3-solutioning/bmad-architecture/SKILL.md +85 -0
  20. package/src/bmm-skills/3-solutioning/bmad-architecture/assets/spine-template.md +79 -0
  21. package/src/bmm-skills/3-solutioning/bmad-architecture/customize.toml +100 -0
  22. package/src/bmm-skills/3-solutioning/bmad-architecture/references/headless.md +26 -0
  23. package/src/bmm-skills/3-solutioning/bmad-architecture/references/reviewer-gate.md +13 -0
  24. package/src/bmm-skills/3-solutioning/bmad-architecture/scripts/lint_spine.py +257 -0
  25. package/src/bmm-skills/3-solutioning/bmad-architecture/scripts/tests/test_lint_spine.py +270 -0
  26. package/src/bmm-skills/3-solutioning/bmad-create-architecture/SKILL.md +16 -60
  27. package/src/bmm-skills/3-solutioning/bmad-create-epics-and-stories/steps/step-01-validate-prerequisites.md +12 -4
  28. package/src/bmm-skills/4-implementation/bmad-code-review/steps/step-02-review.md +1 -1
  29. package/src/bmm-skills/4-implementation/bmad-quick-dev/step-04-review.md +1 -1
  30. package/src/bmm-skills/4-implementation/bmad-retrospective/SKILL.md +29 -14
  31. package/src/bmm-skills/4-implementation/bmad-retrospective/customize.toml +1 -1
  32. package/src/bmm-skills/4-implementation/bmad-sprint-planning/SKILL.md +20 -1
  33. package/src/bmm-skills/4-implementation/bmad-sprint-planning/checklist.md +2 -1
  34. package/src/bmm-skills/4-implementation/bmad-sprint-planning/sprint-status-template.yaml +13 -0
  35. package/src/bmm-skills/4-implementation/bmad-sprint-status/SKILL.md +13 -0
  36. package/src/bmm-skills/module-help.csv +2 -2
  37. package/src/core-skills/bmad-brainstorming/SKILL.md +8 -10
  38. package/src/core-skills/bmad-brainstorming/references/converge.md +1 -1
  39. package/src/core-skills/bmad-brainstorming/references/finalize.md +1 -1
  40. package/src/core-skills/bmad-brainstorming/references/headless.md +4 -4
  41. package/src/core-skills/bmad-brainstorming/references/in-chat-techniques.md +1 -1
  42. package/src/core-skills/bmad-brainstorming/references/mode-autonomous.md +1 -1
  43. package/src/core-skills/bmad-brainstorming/scripts/tests/test_brain.py +2 -2
  44. package/src/core-skills/bmad-customize/scripts/tests/test_list_customizable_skills.py +1 -1
  45. package/src/core-skills/bmad-forge-idea/SKILL.md +79 -0
  46. package/src/core-skills/bmad-forge-idea/customize.toml +42 -0
  47. package/src/core-skills/bmad-forge-idea/scripts/resolve_personas.py +270 -0
  48. package/src/core-skills/bmad-forge-idea/scripts/tests/test_resolve_personas.py +138 -0
  49. package/src/core-skills/bmad-party-mode/SKILL.md +39 -56
  50. package/src/core-skills/bmad-party-mode/customize.toml +175 -0
  51. package/src/core-skills/bmad-party-mode/references/create-party.md +70 -0
  52. package/src/core-skills/bmad-party-mode/references/mode-agent-team.md +11 -0
  53. package/src/core-skills/bmad-party-mode/references/mode-auto.md +13 -0
  54. package/src/core-skills/bmad-party-mode/references/mode-subagent.md +19 -0
  55. package/src/core-skills/bmad-party-mode/references/party-memory.md +51 -0
  56. package/src/core-skills/bmad-party-mode/scripts/resolve_party.py +272 -0
  57. package/src/core-skills/bmad-party-mode/scripts/tests/test-resolve_party.py +146 -0
  58. package/src/core-skills/bmad-spec/SKILL.md +25 -9
  59. package/src/core-skills/bmad-spec/assets/headless-schemas.md +3 -3
  60. package/src/core-skills/bmad-spec/assets/spec-template.md +4 -4
  61. package/src/core-skills/module-help.csv +1 -0
  62. package/src/{core-skills/bmad-brainstorming/scripts → scripts}/memlog.py +56 -34
  63. package/src/scripts/resolve_config.py +8 -6
  64. package/src/scripts/resolve_customization.py +8 -6
  65. package/src/{core-skills/bmad-brainstorming/scripts → scripts}/tests/test_memlog.py +68 -27
  66. package/tools/installer/commands/install.js +3 -0
  67. package/tools/installer/core/installer.js +35 -1
  68. package/tools/installer/core/uv-check.js +97 -0
  69. package/tools/installer/core/wsl-node-check.js +109 -0
  70. package/tools/installer/ide/platform-codes.yaml +14 -0
  71. package/tools/installer/install-messages.yaml +4 -0
  72. package/tools/installer/ui.js +11 -0
  73. package/evals/bmm-skills/bmad-product-brief/evals.json +0 -237
  74. package/evals/bmm-skills/bmad-product-brief/files/branfield-memo.md +0 -46
  75. package/evals/bmm-skills/bmad-product-brief/files/forkbird-brief/addendum.md +0 -40
  76. package/evals/bmm-skills/bmad-product-brief/files/forkbird-brief/brief.md +0 -56
  77. package/evals/bmm-skills/bmad-product-brief/files/forkbird-brief/decision-log.md +0 -27
  78. package/evals/bmm-skills/bmad-product-brief/files/meridian-mobility-report.md +0 -116
  79. package/evals/bmm-skills/bmad-product-brief/files/mossridge-brief/addendum.md +0 -41
  80. package/evals/bmm-skills/bmad-product-brief/files/mossridge-brief/brief.md +0 -57
  81. package/evals/bmm-skills/bmad-product-brief/files/mossridge-brief/decision-log.md +0 -29
  82. package/evals/bmm-skills/bmad-product-brief/files/pantry-bridge-interviews.md +0 -90
  83. package/evals/bmm-skills/bmad-product-brief/files/q2-brainstorm.md +0 -101
  84. package/evals/bmm-skills/bmad-product-brief/triggers.json +0 -18
  85. package/src/bmm-skills/3-solutioning/bmad-create-architecture/architecture-decision-template.md +0 -12
  86. package/src/bmm-skills/3-solutioning/bmad-create-architecture/data/domain-complexity.csv +0 -13
  87. package/src/bmm-skills/3-solutioning/bmad-create-architecture/data/project-types.csv +0 -7
  88. package/src/bmm-skills/3-solutioning/bmad-create-architecture/steps/step-01-init.md +0 -153
  89. package/src/bmm-skills/3-solutioning/bmad-create-architecture/steps/step-01b-continue.md +0 -173
  90. package/src/bmm-skills/3-solutioning/bmad-create-architecture/steps/step-02-context.md +0 -224
  91. package/src/bmm-skills/3-solutioning/bmad-create-architecture/steps/step-03-starter.md +0 -329
  92. package/src/bmm-skills/3-solutioning/bmad-create-architecture/steps/step-04-decisions.md +0 -318
  93. package/src/bmm-skills/3-solutioning/bmad-create-architecture/steps/step-05-patterns.md +0 -359
  94. package/src/bmm-skills/3-solutioning/bmad-create-architecture/steps/step-06-structure.md +0 -379
  95. package/src/bmm-skills/3-solutioning/bmad-create-architecture/steps/step-07-validation.md +0 -361
  96. package/src/bmm-skills/3-solutioning/bmad-create-architecture/steps/step-08-complete.md +0 -82
@@ -5,7 +5,8 @@
5
5
  """Tests for memlog.py. Run: uv run --with pytest pytest scripts/tests/test_memlog.py
6
6
 
7
7
  The spine under test is the flat, append-only, chronological invariant: every entry is
8
- one line recorded at the end in the order it happened — no sections, no grouping.
8
+ one line recorded at the end in the order it happened — no sections, no grouping, and no
9
+ lifecycle status the log would have to mutate.
9
10
  """
10
11
  import json
11
12
  import sys
@@ -60,11 +61,17 @@ def test_init_writes_frontmatter_fields(ws):
60
61
  meta, body = memlog.split(read(ws))
61
62
  assert meta["topic"] == "Reinvent the lunchbox"
62
63
  assert meta["goal"] == "ideas for a pitch"
63
- assert meta["status"] == "active"
64
64
  assert "updated" in meta
65
65
  assert body.strip() == ""
66
66
 
67
67
 
68
+ def test_init_has_no_lifecycle_status(ws):
69
+ # A memory log carries no "status" flag; completion is an appended entry, not frontmatter.
70
+ init(ws)
71
+ meta, _ = memlog.split(read(ws))
72
+ assert "status" not in meta
73
+
74
+
68
75
  def test_init_arbitrary_fields(ws):
69
76
  init(ws, topic="T", audience="board")
70
77
  meta, _ = memlog.split(read(ws))
@@ -86,6 +93,30 @@ def test_init_rejects_malformed_field(ws):
86
93
  assert memlog.main(["init", "--workspace", ws, "--field", "noequals"]) == 2
87
94
 
88
95
 
96
+ # --- addressing: --workspace and --path are interchangeable --------------
97
+
98
+ def test_path_addressing_targets_the_file_directly(tmp_path):
99
+ target = tmp_path / "run" / ".memlog.md"
100
+ assert memlog.main(["init", "--path", str(target), "--field", "topic=T"]) == 0
101
+ assert target.is_file()
102
+ assert memlog.main(["append", "--path", str(target), "--text", "an idea", "--type", "idea"]) == 0
103
+ body = memlog.split(target.read_text(encoding="utf-8"))[1]
104
+ assert "- (idea) an idea" in body
105
+
106
+
107
+ def test_workspace_and_path_resolve_to_same_file(ws):
108
+ init(ws)
109
+ via_path = str(Path(ws) / MEMLOG)
110
+ assert memlog.main(["append", "--path", via_path, "--text", "from path"]) == 0
111
+ assert memlog.main(["append", "--workspace", ws, "--text", "from workspace"]) == 0
112
+ assert entries(ws) == ["- from path", "- from workspace"]
113
+
114
+
115
+ def test_target_is_required(ws):
116
+ with pytest.raises(SystemExit):
117
+ memlog.main(["append", "--text", "orphan"]) # neither --workspace nor --path
118
+
119
+
89
120
  # --- append: flat chronological order is the whole point -----------------
90
121
 
91
122
  def test_append_lands_at_end_in_order(ws):
@@ -119,6 +150,15 @@ def test_append_without_type_is_plain_note(ws):
119
150
  assert entries(ws) == ["- bare entry"]
120
151
 
121
152
 
153
+ def test_completion_is_an_entry_not_a_status(ws):
154
+ # The documented way to mark a session done: append it. Frontmatter never gains a status.
155
+ init(ws)
156
+ append(ws, "session complete", entry_type="event")
157
+ meta, _ = memlog.split(read(ws))
158
+ assert "status" not in meta
159
+ assert entries(ws)[-1] == "- (event) session complete"
160
+
161
+
122
162
  def test_append_collapses_newlines_into_one_line(ws):
123
163
  init(ws)
124
164
  append(ws, "line one\nline two\n spaced out")
@@ -171,29 +211,39 @@ def test_heterogeneous_entry_types_coexist(ws):
171
211
  assert tag in body
172
212
 
173
213
 
174
- # --- set ----------------------------------------------------------------
214
+ def test_free_vocabulary_is_not_enforced(ws):
215
+ # The tool is neutral: any --type the host skill names renders verbatim.
216
+ init(ws)
217
+ append(ws, "a custom kind", entry_type="crack")
218
+ append(ws, "another", entry_type="lock")
219
+ body = body_of(ws)
220
+ assert "- (crack) a custom kind" in body
221
+ assert "- (lock) another" in body
222
+
223
+
224
+ # --- set: generic descriptive frontmatter, no lifecycle semantics --------
175
225
 
176
- def test_set_flips_status(ws):
226
+ def test_set_adds_field(ws):
177
227
  init(ws)
178
- memlog.main(["set", "--workspace", ws, "--key", "status", "--value", "complete"])
179
- assert memlog.split(read(ws))[0]["status"] == "complete"
228
+ memlog.main(["set", "--workspace", ws, "--key", "mode", "--value", "partner"])
229
+ assert memlog.split(read(ws))[0]["mode"] == "partner"
230
+
231
+
232
+ def test_set_replaces_field(ws):
233
+ init(ws, topic="T", mode="facilitator")
234
+ memlog.main(["set", "--workspace", ws, "--key", "mode", "--value", "partner"])
235
+ assert memlog.split(read(ws))[0]["mode"] == "partner"
180
236
 
181
237
 
182
238
  def test_set_preserves_body(ws):
183
239
  init(ws)
184
240
  append(ws, "keep me", entry_type="idea")
185
- memlog.main(["set", "--workspace", ws, "--key", "status", "--value", "complete"])
241
+ memlog.main(["set", "--workspace", ws, "--key", "mode", "--value", "partner"])
186
242
  meta, body = memlog.split(read(ws))
187
- assert meta["status"] == "complete"
243
+ assert meta["mode"] == "partner"
188
244
  assert "- (idea) keep me" in body
189
245
 
190
246
 
191
- def test_set_can_add_new_field(ws):
192
- init(ws)
193
- memlog.main(["set", "--workspace", ws, "--key", "owner", "--value", "BMad"])
194
- assert memlog.split(read(ws))[0]["owner"] == "BMad"
195
-
196
-
197
247
  def test_updated_stays_last(ws):
198
248
  init(ws)
199
249
  memlog.main(["set", "--workspace", ws, "--key", "owner", "--value", "BMad"])
@@ -219,22 +269,14 @@ def test_commas_in_field_survive(ws):
219
269
 
220
270
 
221
271
  def test_triple_dash_in_field_does_not_corrupt_frontmatter(ws):
222
- # A `---` inside a value must NOT be read as the closing fence: topic stays intact,
223
- # status survives, and the body never leaks frontmatter text.
272
+ # A `---` inside a value must NOT be read as the closing fence: topic stays intact
273
+ # and the body never leaks frontmatter text.
224
274
  init(ws, topic="Pricing --- tiers --- and add-ons")
225
275
  append(ws, "an idea", entry_type="idea")
226
276
  meta, body = memlog.split(read(ws))
227
277
  assert meta["topic"] == "Pricing --- tiers --- and add-ons"
228
- assert meta["status"] == "active"
229
278
  assert entries(ws) == ["- (idea) an idea"]
230
- assert "status:" not in body # frontmatter never bled into the body
231
-
232
-
233
- def test_triple_dash_status_survives_in_ack(ws, capsys):
234
- init(ws, topic="a --- b")
235
- append(ws, "x", entry_type="idea")
236
- out = json.loads(capsys.readouterr().out.strip().splitlines()[-1])
237
- assert out["status"] == "active" # not "" — frontmatter recovered cleanly
279
+ assert "topic:" not in body # frontmatter never bled into the body
238
280
 
239
281
 
240
282
  def test_newline_in_field_is_neutralized(ws):
@@ -243,7 +285,6 @@ def test_newline_in_field_is_neutralized(ws):
243
285
  append(ws, "x", entry_type="idea")
244
286
  meta, _ = memlog.split(read(ws))
245
287
  assert "\n" not in meta["topic"]
246
- assert meta["status"] == "active"
247
288
 
248
289
 
249
290
  def test_append_emits_json_ack(ws, capsys):
@@ -251,9 +292,9 @@ def test_append_emits_json_ack(ws, capsys):
251
292
  append(ws, "x", entry_type="idea")
252
293
  out = json.loads(capsys.readouterr().out.strip().splitlines()[-1])
253
294
  assert out["ok"] is True
254
- assert out["status"] == "active"
255
295
  assert out["entries"] == 1
256
296
  assert out["memlog"].endswith(MEMLOG)
297
+ assert "status" not in out # no lifecycle status
257
298
  assert "section" not in out # sections are gone
258
299
 
259
300
 
@@ -75,6 +75,9 @@ module.exports = {
75
75
  return;
76
76
  }
77
77
 
78
+ const { checkWindowsNodeFromWsl } = require('../core/wsl-node-check');
79
+ await checkWindowsNodeFromWsl();
80
+
78
81
  // Set debug flag as environment variable for all components
79
82
  if (options.debug) {
80
83
  process.env.BMAD_DEBUG_MANIFEST = 'true';
@@ -419,10 +419,35 @@ class Installer {
419
419
  const sourceDir = path.dirname(path.join(bmadDir, relativePath));
420
420
  if (await fs.pathExists(sourceDir)) {
421
421
  await fs.remove(sourceDir);
422
+ await this._removeEmptyParents(path.dirname(sourceDir), bmadDir);
422
423
  }
423
424
  }
424
425
  }
425
426
 
427
+ /**
428
+ * Remove now-empty parent directories left behind after skill dir cleanup.
429
+ * Walks up from dir, stopping at (and never removing) bmadDir. Best-effort:
430
+ * a directory that vanishes or fills in mid-walk just ends the walk.
431
+ * @param {string} dir - Directory to start walking up from
432
+ * @param {string} bmadDir - BMAD installation directory (boundary)
433
+ */
434
+ async _removeEmptyParents(dir, bmadDir) {
435
+ let current = dir;
436
+ while (true) {
437
+ // Path-boundary check (not a string prefix, so siblings like _bmad2 don't match).
438
+ const rel = path.relative(bmadDir, current);
439
+ if (rel === '' || rel.startsWith('..') || path.isAbsolute(rel)) break;
440
+ try {
441
+ const entries = await fs.readdir(current);
442
+ if (entries.length > 0) break;
443
+ await fs.rmdir(current);
444
+ } catch {
445
+ break;
446
+ }
447
+ current = path.dirname(current);
448
+ }
449
+ }
450
+
426
451
  async _readSkillManifestRows(bmadDir) {
427
452
  const csvPath = path.join(bmadDir, '_config', 'skill-manifest.csv');
428
453
  if (!(await fs.pathExists(csvPath))) return [];
@@ -630,6 +655,7 @@ class Installer {
630
655
  /**
631
656
  * Sync src/scripts/* → _bmad/scripts/ so shared Python scripts
632
657
  * (e.g. resolve_customization.py) are available at install time.
658
+ * Excludes dev-only tests and Python caches so they don't ship to users.
633
659
  * Wipes the destination first so files removed or renamed in source
634
660
  * don't linger and get recorded as installed. Also seeds
635
661
  * _bmad/custom/.gitignore on fresh installs so *.user.toml overrides
@@ -643,7 +669,12 @@ class Installer {
643
669
 
644
670
  await fs.remove(paths.scriptsDir);
645
671
  await fs.ensureDir(paths.scriptsDir);
646
- await fs.copy(srcScriptsDir, paths.scriptsDir, { overwrite: true });
672
+ // Ship only the runtime scripts — dev-only tests and Python caches must not land in user projects.
673
+ const isInstallable = (srcPath) => {
674
+ const base = path.basename(srcPath);
675
+ return base !== 'tests' && base !== '__pycache__' && base !== '.pytest_cache' && !base.endsWith('.pyc');
676
+ };
677
+ await fs.copy(srcScriptsDir, paths.scriptsDir, { overwrite: true, filter: isInstallable });
647
678
  await this._trackFilesRecursive(paths.scriptsDir);
648
679
 
649
680
  const customGitignore = path.join(paths.customDir, '.gitignore');
@@ -1202,6 +1233,9 @@ class Installer {
1202
1233
  ` 1. Launch your AI agent from your project folder`,
1203
1234
  ` 2. Not sure what to do? Invoke the ${color.cyan('bmad-help')} skill and ask it what to do!`,
1204
1235
  '',
1236
+ ` ${color.cyan('Tip:')} BMAD workflows increasingly run Python scripts via ${color.cyan('uv run')} — uv is`,
1237
+ ` becoming the de facto standard. If you don't have it yet, ask your agent to set it up.`,
1238
+ '',
1205
1239
  ` Blog, Docs and Guides: ${color.blue('https://bmadcode.com/')}`,
1206
1240
  ` Community: ${color.blue('https://discord.gg/gk8jAdXWmj')}`,
1207
1241
  );
@@ -0,0 +1,97 @@
1
+ const { spawnSync } = require('node:child_process');
2
+ const prompts = require('../prompts');
3
+
4
+ // `uv` (https://docs.astral.sh/uv/) is becoming the de facto standard for
5
+ // running the Python scripts BMAD workflows shell out to: `uv run <script>`
6
+ // resolves the interpreter and any dependencies on demand, so skills don't
7
+ // have to assume a particular `python3` is on PATH. The ecosystem is mid-
8
+ // migration — some skills still call `python3` directly — so a missing `uv`
9
+ // is a warning, not a blocker: BMAD installs and runs either way.
10
+ const RUNTIME_COMMAND = 'uv';
11
+
12
+ /**
13
+ * Parse `uv --version` output into version parts.
14
+ * Example outputs: "uv 0.5.31", "uv 0.5.31 (Homebrew 2025-02-12)".
15
+ * @param {string} output - stdout/stderr from `uv --version`
16
+ * @returns {{major: number, minor: number, patch: number, raw: string}|null}
17
+ */
18
+ function parseUvVersion(output) {
19
+ if (!output) return null;
20
+ const match = output.match(/uv\s+(\d+)\.(\d+)(?:\.(\d+))?/i);
21
+ if (!match) return null;
22
+ return {
23
+ major: Number(match[1]),
24
+ minor: Number(match[2]),
25
+ patch: Number(match[3] || 0),
26
+ raw: `${match[1]}.${match[2]}.${match[3] || 0}`,
27
+ };
28
+ }
29
+
30
+ /**
31
+ * Probe the local environment for `uv`.
32
+ * @returns {{version: {major: number, minor: number, patch: number, raw: string}}|null}
33
+ */
34
+ function detectUv() {
35
+ let result;
36
+ try {
37
+ result = spawnSync(RUNTIME_COMMAND, ['--version'], {
38
+ encoding: 'utf8',
39
+ timeout: 5000,
40
+ windowsHide: true,
41
+ });
42
+ } catch {
43
+ return null;
44
+ }
45
+ if (!result || result.error) return null;
46
+ const version = parseUvVersion(`${result.stdout || ''}\n${result.stderr || ''}`);
47
+ return version ? { version } : null;
48
+ }
49
+
50
+ function setupHints() {
51
+ return [
52
+ 'BMAD workflows increasingly run Python scripts via `uv run`, which manages',
53
+ 'the interpreter and dependencies for you — no manual venv or pip needed.',
54
+ '',
55
+ 'Easiest path: ask your AI agent to "install and set up uv for me".',
56
+ '',
57
+ 'Or install it yourself:',
58
+ ' macOS/Linux: curl -LsSf https://astral.sh/uv/install.sh | sh',
59
+ ' Windows: powershell -c "irm https://astral.sh/uv/install.ps1 | iex"',
60
+ ' Homebrew: brew install uv',
61
+ ' Docs: https://docs.astral.sh/uv/getting-started/installation/',
62
+ ].join('\n');
63
+ }
64
+
65
+ /**
66
+ * Check whether `uv` is available and inform the user.
67
+ *
68
+ * Warn-don't-block, and no acknowledgement prompt: `uv` is on its way to being
69
+ * the standard runner for BMAD's Python scripts, but the migration is still in
70
+ * progress, so the install never stops on its account. The note tells the user
71
+ * how to set it up (preferably by asking their agent).
72
+ *
73
+ * @returns {Promise<{status: 'found'|'missing', detected: Object|null}>}
74
+ */
75
+ async function checkUvEnvironment() {
76
+ // Called via module.exports so tests can stub detection.
77
+ const detected = module.exports.detectUv();
78
+
79
+ if (detected) {
80
+ await prompts.log.success(`uv ${detected.version.raw} detected — ready to run BMAD's Python-powered scripts via \`uv run\`.`);
81
+ return { status: 'found', detected };
82
+ }
83
+
84
+ await prompts.log.warn(
85
+ "uv not found on PATH. uv is becoming the de facto standard for running BMAD's Python\n" +
86
+ 'scripts (`uv run <script>`), and it provisions the interpreter for you. BMAD installs\n' +
87
+ 'fine without it, but setting up uv now keeps you ahead as workflows adopt it.',
88
+ );
89
+ await prompts.note(setupHints(), 'uv recommended');
90
+ return { status: 'missing', detected: null };
91
+ }
92
+
93
+ module.exports = {
94
+ checkUvEnvironment,
95
+ detectUv,
96
+ parseUvVersion,
97
+ };
@@ -0,0 +1,109 @@
1
+ const prompts = require('../prompts');
2
+
3
+ const WSL_UNC_PATTERN = /^\\\\wsl(?:\.localhost|\$)?\\/i;
4
+
5
+ function normalizePath(value) {
6
+ return typeof value === 'string' ? value.replaceAll('/', '\\').toLowerCase() : '';
7
+ }
8
+
9
+ function isLinuxStylePath(value) {
10
+ return (
11
+ typeof value === 'string' &&
12
+ value.startsWith('/') &&
13
+ !value.startsWith('//') &&
14
+ !/^\/[a-z](?:\/|$)/i.test(value) &&
15
+ !/^\/cygdrive\/[a-z](?:\/|$)/i.test(value)
16
+ );
17
+ }
18
+
19
+ function isWslUncPath(value) {
20
+ return WSL_UNC_PATTERN.test(value || '');
21
+ }
22
+
23
+ /**
24
+ * Detect the broken interop case where WSL resolved node/npx to Windows.
25
+ * @param {Object} [runtime]
26
+ * @param {string} [runtime.platform]
27
+ * @param {Object} [runtime.env]
28
+ * @param {string} [runtime.cwd]
29
+ * @param {string} [runtime.execPath]
30
+ * @returns {{isMismatch: boolean, reason: string|null, execPath: string}}
31
+ */
32
+ function detectWindowsNodeFromWsl(runtime = {}) {
33
+ const platform = runtime.platform || process.platform;
34
+ const env = runtime.env || process.env;
35
+ const cwd = runtime.cwd || safeCwd();
36
+ const execPath = runtime.execPath || process.execPath || '';
37
+
38
+ if (platform !== 'win32') {
39
+ return { isMismatch: false, reason: null, execPath };
40
+ }
41
+
42
+ if (env.WSL_DISTRO_NAME) {
43
+ return { isMismatch: true, reason: 'WSL_DISTRO_NAME is set', execPath };
44
+ }
45
+
46
+ if (env.WSL_INTEROP) {
47
+ return { isMismatch: true, reason: 'WSL_INTEROP is set', execPath };
48
+ }
49
+
50
+ if (isLinuxStylePath(env.PWD)) {
51
+ return { isMismatch: true, reason: 'PWD is a Linux path', execPath };
52
+ }
53
+
54
+ if (isWslUncPath(cwd)) {
55
+ return { isMismatch: true, reason: 'current directory is a WSL UNC path', execPath };
56
+ }
57
+
58
+ const normalizedExecPath = normalizePath(execPath);
59
+ if (normalizedExecPath.includes('\\wsl$\\') || normalizedExecPath.includes('\\wsl.localhost\\')) {
60
+ return { isMismatch: true, reason: 'Node executable path is under a WSL UNC path', execPath };
61
+ }
62
+
63
+ return { isMismatch: false, reason: null, execPath };
64
+ }
65
+
66
+ function safeCwd() {
67
+ try {
68
+ return process.cwd();
69
+ } catch {
70
+ return '';
71
+ }
72
+ }
73
+
74
+ function formatWindowsNodeFromWslMessage(detection) {
75
+ const lines = [
76
+ 'Windows Node.js was launched from a WSL shell.',
77
+ '',
78
+ 'This usually means Node.js is not installed inside the WSL distro, so WSL resolved `node`/`npx` to Windows.',
79
+ 'The installer cannot safely continue because Linux paths may be interpreted as Windows paths.',
80
+ '',
81
+ 'Install Node.js inside WSL, then rerun the same command from the WSL terminal.',
82
+ ];
83
+
84
+ if (detection.execPath) {
85
+ lines.push('', `Detected Node executable: ${detection.execPath}`);
86
+ }
87
+
88
+ if (detection.reason) {
89
+ lines.push(`Detection signal: ${detection.reason}`);
90
+ }
91
+
92
+ return lines.join('\n');
93
+ }
94
+
95
+ async function checkWindowsNodeFromWsl() {
96
+ const detection = module.exports.detectWindowsNodeFromWsl();
97
+ if (!detection.isMismatch) {
98
+ return detection;
99
+ }
100
+
101
+ await prompts.log.error(formatWindowsNodeFromWslMessage(detection));
102
+ process.exit(1);
103
+ }
104
+
105
+ module.exports = {
106
+ checkWindowsNodeFromWsl,
107
+ detectWindowsNodeFromWsl,
108
+ formatWindowsNodeFromWslMessage,
109
+ };
@@ -70,6 +70,13 @@ platforms:
70
70
  target_dir: .agents/skills
71
71
  global_target_dir: ~/.codex/skills
72
72
 
73
+ codewhale:
74
+ name: "CodeWhale"
75
+ preferred: false
76
+ installer:
77
+ target_dir: .codewhale/skills
78
+ global_target_dir: ~/.codewhale/skills
79
+
73
80
  codebuddy:
74
81
  name: "CodeBuddy"
75
82
  preferred: false
@@ -155,6 +162,13 @@ platforms:
155
162
  target_dir: .agents/skills
156
163
  global_target_dir: ~/.config/agents/skills
157
164
 
165
+ hermes:
166
+ name: "Hermes Agent"
167
+ preferred: false
168
+ installer:
169
+ target_dir: .agents/skills
170
+ global_target_dir: ~/.hermes/skills
171
+
158
172
  iflow:
159
173
  name: "iFlow"
160
174
  preferred: false
@@ -12,6 +12,10 @@ startMessage: |
12
12
  🌟 100% free. 100% open source. Always.
13
13
  No paywalls. No gated content. Knowledge shared, not sold.
14
14
 
15
+ 🐍 HEADS UP: uv (https://docs.astral.sh/uv/) is becoming the de facto standard
16
+ for running the Python scripts BMAD workflows rely on (`uv run <script>`).
17
+ If it's not set up yet, ask your AI agent to "install and set up uv for me".
18
+
15
19
  🌐 CONNECT:
16
20
  Website: https://bmadcode.com/
17
21
  Discord: https://discord.gg/gk8jAdXWmj
@@ -161,6 +161,17 @@ class UI {
161
161
  const messageLoader = new MessageLoader();
162
162
  await messageLoader.displayStartMessage();
163
163
 
164
+ // Probe for `uv` before any other prompts: it's becoming the de facto
165
+ // runner for the Python scripts BMAD workflows shell out to
166
+ // (`uv run <script>`), and uv provisions the interpreter itself, so it's
167
+ // the single thing worth checking for. The migration is still in progress
168
+ // (some skills still call `python3` directly), so this is informational —
169
+ // warn-don't-block, no ack prompt — and just points the user at setup
170
+ // (ideally "ask your agent to set up uv"). The installer runs in the
171
+ // destination environment, so probing PATH here tests the right machine.
172
+ const { checkUvEnvironment } = require('./core/uv-check');
173
+ await checkUvEnvironment();
174
+
164
175
  // Parse channel flags (--channel/--all-*/--next=/--pin) once. Warnings
165
176
  // are surfaced immediately so the user sees them before any git ops run.
166
177
  const channelOptions = parseChannelOptions(options);