bmad-method 6.8.1-next.8 → 6.9.0

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 (91) 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/4-implementation/bmad-retrospective/SKILL.md +15 -0
  28. package/src/bmm-skills/4-implementation/bmad-sprint-planning/SKILL.md +20 -1
  29. package/src/bmm-skills/4-implementation/bmad-sprint-planning/checklist.md +2 -1
  30. package/src/bmm-skills/4-implementation/bmad-sprint-planning/sprint-status-template.yaml +13 -0
  31. package/src/bmm-skills/4-implementation/bmad-sprint-status/SKILL.md +13 -0
  32. package/src/bmm-skills/module-help.csv +2 -2
  33. package/src/core-skills/bmad-brainstorming/SKILL.md +8 -10
  34. package/src/core-skills/bmad-brainstorming/references/converge.md +1 -1
  35. package/src/core-skills/bmad-brainstorming/references/finalize.md +1 -1
  36. package/src/core-skills/bmad-brainstorming/references/headless.md +4 -4
  37. package/src/core-skills/bmad-brainstorming/references/in-chat-techniques.md +1 -1
  38. package/src/core-skills/bmad-brainstorming/references/mode-autonomous.md +1 -1
  39. package/src/core-skills/bmad-brainstorming/scripts/tests/test_brain.py +2 -2
  40. package/src/core-skills/bmad-customize/scripts/tests/test_list_customizable_skills.py +1 -1
  41. package/src/core-skills/bmad-forge-idea/SKILL.md +79 -0
  42. package/src/core-skills/bmad-forge-idea/customize.toml +42 -0
  43. package/src/core-skills/bmad-forge-idea/scripts/resolve_personas.py +270 -0
  44. package/src/core-skills/bmad-forge-idea/scripts/tests/test_resolve_personas.py +138 -0
  45. package/src/core-skills/bmad-party-mode/SKILL.md +39 -56
  46. package/src/core-skills/bmad-party-mode/customize.toml +175 -0
  47. package/src/core-skills/bmad-party-mode/references/create-party.md +70 -0
  48. package/src/core-skills/bmad-party-mode/references/mode-agent-team.md +11 -0
  49. package/src/core-skills/bmad-party-mode/references/mode-auto.md +13 -0
  50. package/src/core-skills/bmad-party-mode/references/mode-subagent.md +19 -0
  51. package/src/core-skills/bmad-party-mode/references/party-memory.md +51 -0
  52. package/src/core-skills/bmad-party-mode/scripts/resolve_party.py +272 -0
  53. package/src/core-skills/bmad-party-mode/scripts/tests/test-resolve_party.py +146 -0
  54. package/src/core-skills/bmad-spec/SKILL.md +5 -3
  55. package/src/core-skills/bmad-spec/assets/spec-template.md +3 -3
  56. package/src/core-skills/module-help.csv +1 -0
  57. package/src/scripts/resolve_config.py +8 -6
  58. package/src/scripts/resolve_customization.py +8 -6
  59. package/tools/installer/commands/install.js +3 -0
  60. package/tools/installer/core/installer.js +3 -0
  61. package/tools/installer/core/uv-check.js +97 -0
  62. package/tools/installer/core/wsl-node-check.js +109 -0
  63. package/tools/installer/ide/platform-codes.yaml +7 -0
  64. package/tools/installer/install-messages.yaml +4 -0
  65. package/tools/installer/ui.js +11 -0
  66. package/evals/bmm-skills/bmad-product-brief/evals.json +0 -237
  67. package/evals/bmm-skills/bmad-product-brief/files/branfield-memo.md +0 -46
  68. package/evals/bmm-skills/bmad-product-brief/files/forkbird-brief/addendum.md +0 -40
  69. package/evals/bmm-skills/bmad-product-brief/files/forkbird-brief/brief.md +0 -56
  70. package/evals/bmm-skills/bmad-product-brief/files/forkbird-brief/decision-log.md +0 -27
  71. package/evals/bmm-skills/bmad-product-brief/files/meridian-mobility-report.md +0 -116
  72. package/evals/bmm-skills/bmad-product-brief/files/mossridge-brief/addendum.md +0 -41
  73. package/evals/bmm-skills/bmad-product-brief/files/mossridge-brief/brief.md +0 -57
  74. package/evals/bmm-skills/bmad-product-brief/files/mossridge-brief/decision-log.md +0 -29
  75. package/evals/bmm-skills/bmad-product-brief/files/pantry-bridge-interviews.md +0 -90
  76. package/evals/bmm-skills/bmad-product-brief/files/q2-brainstorm.md +0 -101
  77. package/evals/bmm-skills/bmad-product-brief/triggers.json +0 -18
  78. package/src/bmm-skills/3-solutioning/bmad-create-architecture/architecture-decision-template.md +0 -12
  79. package/src/bmm-skills/3-solutioning/bmad-create-architecture/data/domain-complexity.csv +0 -13
  80. package/src/bmm-skills/3-solutioning/bmad-create-architecture/data/project-types.csv +0 -7
  81. package/src/bmm-skills/3-solutioning/bmad-create-architecture/steps/step-01-init.md +0 -153
  82. package/src/bmm-skills/3-solutioning/bmad-create-architecture/steps/step-01b-continue.md +0 -173
  83. package/src/bmm-skills/3-solutioning/bmad-create-architecture/steps/step-02-context.md +0 -224
  84. package/src/bmm-skills/3-solutioning/bmad-create-architecture/steps/step-03-starter.md +0 -329
  85. package/src/bmm-skills/3-solutioning/bmad-create-architecture/steps/step-04-decisions.md +0 -318
  86. package/src/bmm-skills/3-solutioning/bmad-create-architecture/steps/step-05-patterns.md +0 -359
  87. package/src/bmm-skills/3-solutioning/bmad-create-architecture/steps/step-06-structure.md +0 -379
  88. package/src/bmm-skills/3-solutioning/bmad-create-architecture/steps/step-07-validation.md +0 -361
  89. package/src/bmm-skills/3-solutioning/bmad-create-architecture/steps/step-08-complete.md +0 -82
  90. package/src/core-skills/bmad-brainstorming/scripts/memlog.py +0 -202
  91. package/src/core-skills/bmad-brainstorming/scripts/tests/test_memlog.py +0 -265
@@ -0,0 +1,272 @@
1
+ #!/usr/bin/env python3
2
+ # /// script
3
+ # requires-python = ">=3.11"
4
+ # ///
5
+ """Resolve the party-mode roster, lazily.
6
+
7
+ Merges the installed BMAD agents with the user's custom `party_members`
8
+ into one collective, then projects only what the moment needs:
9
+
10
+ * default (no flag) — the active roster to load on entry: the
11
+ `default_party` group if one is configured, else the whole collective.
12
+ Other groups come back as names only, so nothing you aren't using is
13
+ loaded into the party.
14
+ * --list-groups — just id + name + size for every configured group. The
15
+ cheap menu for "which room?", with no member detail.
16
+ * --party <id> — full member detail for one chosen group, on demand
17
+ (e.g. when the user switches rooms). Unknown id returns the available
18
+ names instead of an error wall.
19
+
20
+ The merge is deterministic (a keyed union; a custom member whose code
21
+ matches an installed agent overrides it), so the orchestrator consumes a
22
+ resolved roster instead of re-deriving it every session.
23
+
24
+ Stdlib only (Python 3.11+ for tomllib). Shells out to the project's
25
+ resolve_config.py and resolve_customization.py; falls back to reading
26
+ customize.toml directly if the customization resolver is unavailable.
27
+
28
+ resolve_party.py --project-root P --skill S
29
+ resolve_party.py --project-root P --skill S --list-groups
30
+ resolve_party.py --project-root P --skill S --party writers-room
31
+ """
32
+
33
+ import argparse
34
+ import json
35
+ import subprocess
36
+ import sys
37
+ from pathlib import Path
38
+
39
+ try:
40
+ import tomllib
41
+ except ImportError: # pragma: no cover - guarded for <3.11
42
+ sys.stderr.write("error: Python 3.11+ is required (stdlib `tomllib`).\n")
43
+ sys.exit(3)
44
+
45
+
46
+ def _run_json(cmd):
47
+ """Run a resolver script and parse its JSON stdout. None on any failure."""
48
+ try:
49
+ out = subprocess.run(cmd, capture_output=True, text=True, timeout=60)
50
+ except (OSError, subprocess.SubprocessError):
51
+ return None
52
+ if out.returncode != 0 or not out.stdout.strip():
53
+ return None
54
+ try:
55
+ return json.loads(out.stdout)
56
+ except json.JSONDecodeError:
57
+ return None
58
+
59
+
60
+ def load_agents(project_root: Path):
61
+ """Installed agents as {code: entry}. Empty dict (with a flag) on failure."""
62
+ script = project_root / "_bmad" / "scripts" / "resolve_config.py"
63
+ data = _run_json([sys.executable, str(script), "--project-root", str(project_root), "--key", "agents"])
64
+ if data is None:
65
+ return {}, False
66
+ return data.get("agents", {}) or {}, True
67
+
68
+
69
+ def load_workflow(project_root: Path, skill_root: Path):
70
+ """Merged [workflow] table. Falls back to the skill's base customize.toml."""
71
+ script = project_root / "_bmad" / "scripts" / "resolve_customization.py"
72
+ data = _run_json([sys.executable, str(script), "--skill", str(skill_root), "--key", "workflow"])
73
+ if data is not None and "workflow" in data:
74
+ return data["workflow"]
75
+ # Fallback: read the skill's base customize.toml directly (no override merge).
76
+ toml_path = skill_root / "customize.toml"
77
+ if toml_path.exists():
78
+ try:
79
+ with toml_path.open("rb") as f:
80
+ return tomllib.load(f).get("workflow", {})
81
+ except (OSError, tomllib.TOMLDecodeError):
82
+ pass
83
+ return {}
84
+
85
+
86
+ def _alias(code: str) -> str:
87
+ """Short alias for an installed agent code: bmad-agent-analyst -> analyst."""
88
+ for prefix in ("bmad-agent-", "bmad-"):
89
+ if code.startswith(prefix):
90
+ return code[len(prefix):]
91
+ return code
92
+
93
+
94
+ def build_collective(agents: dict, party_members: list):
95
+ """One pool keyed by code. Custom members override matching installed agents.
96
+
97
+ Returns (collective, index, installed_codes):
98
+ * collective — every member (installed + custom), the pool groups draw
99
+ from and the orchestrator can summon by name.
100
+ * index — maps every resolvable token (code, prefix-stripped alias,
101
+ lower-cased name) to a canonical code.
102
+ * installed_codes — the codes occupying an installed-agent slot, in
103
+ order. This is the DEFAULT room: installed agents (with any custom
104
+ override applied in place), and NOT the pure-custom additions. So
105
+ shipping or defining custom members grows the pool without crowding
106
+ the default party.
107
+ """
108
+ collective = {}
109
+ index = {}
110
+ installed_codes = []
111
+
112
+ def register(code, entry):
113
+ collective[code] = entry
114
+ index[code] = code
115
+ index[code.lower()] = code
116
+ index[_alias(code).lower()] = code
117
+ name = entry.get("name")
118
+ if name:
119
+ index[name.lower()] = code
120
+
121
+ for code, info in agents.items():
122
+ register(code, {
123
+ "code": code,
124
+ "name": info.get("name", code),
125
+ "icon": info.get("icon", ""),
126
+ "title": info.get("title", ""),
127
+ "description": info.get("description", ""),
128
+ "module": info.get("module", ""),
129
+ "team": info.get("team", ""),
130
+ "source": "installed",
131
+ })
132
+ installed_codes.append(code)
133
+
134
+ for m in party_members or []:
135
+ code = m.get("code")
136
+ if not code:
137
+ continue
138
+ # A custom member overrides an installed agent it matches by code/alias/name.
139
+ canonical = index.get(code) or index.get(code.lower()) or code
140
+ entry = {"code": canonical, "source": "custom"}
141
+ for field in ("name", "icon", "title", "persona", "capabilities", "model"):
142
+ if m.get(field) is not None:
143
+ entry[field] = m[field]
144
+ entry.setdefault("name", canonical)
145
+ register(canonical, entry)
146
+ # An override keeps the installed slot; a brand-new custom does not join it.
147
+
148
+ return collective, index, installed_codes
149
+
150
+
151
+ def resolve_members(member_tokens, collective, index):
152
+ """(resolved entries in listed order, unresolved tokens)."""
153
+ resolved, unresolved = [], []
154
+ for token in member_tokens or []:
155
+ code = index.get(token) or index.get(str(token).lower())
156
+ if code and code in collective:
157
+ resolved.append(collective[code])
158
+ else:
159
+ unresolved.append(token)
160
+ return resolved, unresolved
161
+
162
+
163
+ def group_menu(groups):
164
+ """Names only — the cheap menu. Open-cast groups (no roster) are flagged."""
165
+ out = []
166
+ for g in groups or []:
167
+ if not isinstance(g, dict) or not g.get("id"):
168
+ continue
169
+ members = g.get("members", []) or []
170
+ entry = {"id": g["id"], "name": g.get("name", g["id"]),
171
+ "member_count": len(members)}
172
+ if not members:
173
+ entry["open_cast"] = True
174
+ out.append(entry)
175
+ return out
176
+
177
+
178
+ def find_group(groups, group_id):
179
+ for g in groups or []:
180
+ if isinstance(g, dict) and g.get("id") == group_id:
181
+ return g
182
+ return None
183
+
184
+
185
+ def group_detail(g, collective, index):
186
+ """Full detail for one group: resolved members + the optional scene.
187
+
188
+ `scene` is a freeform line the orchestrator plays — setting, what's
189
+ happening, room dynamics, in-the-moment character notes. Surfaced only
190
+ here (when a group is the active/chosen roster), never in the menu.
191
+
192
+ `members` is optional. With none, the group is open-cast: `open_cast`
193
+ is flagged and the scene describes the pool the orchestrator casts from
194
+ on the fly (e.g. "figures from the Star Wars Rebels universe"). A few
195
+ listed members anchor the room; the scene can still invite more.
196
+ """
197
+ raw_members = g.get("members", []) or []
198
+ members, unresolved = resolve_members(raw_members, collective, index)
199
+ detail = {"active": g["id"], "name": g.get("name", g["id"]),
200
+ "members": members, "unresolved": unresolved,
201
+ "memory_enabled": bool(g.get("memory", False))}
202
+ if g.get("scene"):
203
+ detail["scene"] = g["scene"]
204
+ if not raw_members:
205
+ detail["open_cast"] = True
206
+ return detail
207
+
208
+
209
+ def main():
210
+ ap = argparse.ArgumentParser(description="Resolve the party-mode roster, lazily.")
211
+ ap.add_argument("--project-root", required=True)
212
+ ap.add_argument("--skill", required=True, help="Path to the bmad-party-mode skill dir")
213
+ ap.add_argument("--party", help="Resolve full detail for this group id")
214
+ ap.add_argument("--list-groups", action="store_true", help="Group names only")
215
+ args = ap.parse_args()
216
+
217
+ project_root = Path(args.project_root).resolve()
218
+ skill_root = Path(args.skill).resolve()
219
+
220
+ workflow = load_workflow(project_root, skill_root)
221
+ groups = workflow.get("party_groups", []) or []
222
+ default_party = workflow.get("default_party", "") or ""
223
+ party_mode = workflow.get("party_mode", "session") or "session"
224
+ # The global party_memory flag governs only the DEFAULT installed-agent room;
225
+ # a named group carries its own `memory` flag (resolved in group_detail).
226
+ party_memory = bool(workflow.get("party_memory", True))
227
+
228
+ # Group menu never needs the (more expensive) installed-agent resolve.
229
+ if args.list_groups:
230
+ _emit({
231
+ "party_mode": party_mode,
232
+ "default_party": default_party,
233
+ "groups": group_menu(groups),
234
+ })
235
+ return
236
+
237
+ agents, agents_ok = load_agents(project_root)
238
+ collective, index, installed_codes = build_collective(agents, workflow.get("party_members", []))
239
+
240
+ if args.party:
241
+ g = find_group(groups, args.party)
242
+ if g is None:
243
+ _emit({"error": "unknown_group", "requested": args.party,
244
+ "available": group_menu(groups)})
245
+ return
246
+ _emit({**group_detail(g, collective, index), "party_mode": party_mode})
247
+ return
248
+
249
+ # Default: the active roster to load on entry.
250
+ result = {"party_mode": party_mode, "groups": group_menu(groups),
251
+ "installed_agents_resolved": agents_ok}
252
+ g = find_group(groups, default_party) if default_party else None
253
+ if g is not None:
254
+ result.update(group_detail(g, collective, index))
255
+ else:
256
+ # No default group: the installed agents (custom additions stay in the
257
+ # pool but don't crowd the default room), exactly like a plain install.
258
+ result.update({"active": "installed",
259
+ "members": [collective[c] for c in installed_codes],
260
+ "memory_enabled": party_memory})
261
+ _emit(result)
262
+
263
+
264
+ def _emit(obj):
265
+ reconfigure = getattr(sys.stdout, "reconfigure", None)
266
+ if reconfigure is not None:
267
+ reconfigure(encoding="utf-8")
268
+ sys.stdout.write(json.dumps(obj, indent=2, ensure_ascii=False) + "\n")
269
+
270
+
271
+ if __name__ == "__main__":
272
+ main()
@@ -0,0 +1,146 @@
1
+ #!/usr/bin/env python3
2
+ # /// script
3
+ # requires-python = ">=3.11"
4
+ # ///
5
+ """Unit tests for resolve_party.py — merge, alias, override, group resolution."""
6
+
7
+ import sys
8
+ import unittest
9
+ from pathlib import Path
10
+
11
+ sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
12
+ import resolve_party as rp # noqa: E402
13
+
14
+ AGENTS = {
15
+ "bmad-agent-analyst": {"name": "Mary", "icon": "📊", "title": "Analyst"},
16
+ "bmad-agent-pm": {"name": "John", "icon": "📋", "title": "PM"},
17
+ }
18
+
19
+
20
+ class TestAlias(unittest.TestCase):
21
+ def test_strips_known_prefixes(self):
22
+ self.assertEqual(rp._alias("bmad-agent-analyst"), "analyst")
23
+ self.assertEqual(rp._alias("bmad-foo"), "foo")
24
+
25
+ def test_passes_through_unprefixed(self):
26
+ self.assertEqual(rp._alias("morpheus"), "morpheus")
27
+
28
+
29
+ class TestBuildCollective(unittest.TestCase):
30
+ def test_installed_agents_indexed_by_code_alias_and_name(self):
31
+ col, idx, _ = rp.build_collective(AGENTS, [])
32
+ self.assertEqual(set(col), {"bmad-agent-analyst", "bmad-agent-pm"})
33
+ self.assertEqual(idx["analyst"], "bmad-agent-analyst") # alias
34
+ self.assertEqual(idx["mary"], "bmad-agent-analyst") # name (ci)
35
+ self.assertEqual(idx["bmad-agent-pm"], "bmad-agent-pm") # full code
36
+ self.assertEqual(col["bmad-agent-analyst"]["source"], "installed")
37
+
38
+ def test_custom_member_appends(self):
39
+ col, _, _ = rp.build_collective(AGENTS, [{"code": "morpheus", "name": "Morpheus", "persona": "riddles"}])
40
+ self.assertIn("morpheus", col)
41
+ self.assertEqual(col["morpheus"]["source"], "custom")
42
+ self.assertEqual(col["morpheus"]["persona"], "riddles")
43
+
44
+ def test_custom_overrides_installed_by_alias(self):
45
+ col, _, _ = rp.build_collective(AGENTS, [{"code": "analyst", "name": "Mary-Custom", "persona": "p"}])
46
+ # Override lands on the canonical installed code, not a new "analyst" entry.
47
+ self.assertNotIn("analyst", col)
48
+ self.assertEqual(col["bmad-agent-analyst"]["source"], "custom")
49
+ self.assertEqual(col["bmad-agent-analyst"]["name"], "Mary-Custom")
50
+
51
+ def test_member_without_code_skipped(self):
52
+ col, _, _ = rp.build_collective(AGENTS, [{"name": "Nameless"}])
53
+ self.assertEqual(set(col), {"bmad-agent-analyst", "bmad-agent-pm"})
54
+
55
+
56
+ class TestResolveMembers(unittest.TestCase):
57
+ def setUp(self):
58
+ self.col, self.idx, _ = rp.build_collective(AGENTS, [{"code": "morpheus", "name": "Morpheus"}])
59
+
60
+ def test_resolves_in_listed_order_and_flags_unknowns(self):
61
+ resolved, unresolved = rp.resolve_members(["morpheus", "analyst", "ghost"], self.col, self.idx)
62
+ self.assertEqual([m["code"] for m in resolved], ["morpheus", "bmad-agent-analyst"])
63
+ self.assertEqual(unresolved, ["ghost"])
64
+
65
+ def test_empty(self):
66
+ self.assertEqual(rp.resolve_members([], self.col, self.idx), ([], []))
67
+
68
+
69
+ class TestGroups(unittest.TestCase):
70
+ GROUPS = [
71
+ {"id": "wr", "name": "Writers", "members": ["analyst", "morpheus"]},
72
+ {"id": "bad"}, # no name -> falls back to id; no members -> count 0
73
+ {"name": "no-id"}, # dropped from menu
74
+ ]
75
+
76
+ def test_menu_is_names_only_with_counts_and_open_cast_flag(self):
77
+ menu = rp.group_menu(self.GROUPS)
78
+ self.assertEqual(menu, [
79
+ {"id": "wr", "name": "Writers", "member_count": 2},
80
+ {"id": "bad", "name": "bad", "member_count": 0, "open_cast": True},
81
+ ])
82
+
83
+ def test_find_group(self):
84
+ self.assertEqual(rp.find_group(self.GROUPS, "wr")["name"], "Writers")
85
+ self.assertIsNone(rp.find_group(self.GROUPS, "missing"))
86
+
87
+
88
+ class TestGroupDetail(unittest.TestCase):
89
+ def setUp(self):
90
+ self.col, self.idx, _ = rp.build_collective(AGENTS, [{"code": "morpheus", "name": "Morpheus"}])
91
+
92
+ def test_scene_passes_through_when_present(self):
93
+ g = {"id": "tos-10-forward", "name": "Ten Forward", "members": ["morpheus"],
94
+ "scene": "Late evening, a few rounds in."}
95
+ d = rp.group_detail(g, self.col, self.idx)
96
+ self.assertEqual(d["scene"], "Late evening, a few rounds in.")
97
+ self.assertEqual([m["code"] for m in d["members"]], ["morpheus"])
98
+
99
+ def test_scene_omitted_when_absent_or_empty(self):
100
+ for g in ({"id": "g", "members": ["morpheus"]},
101
+ {"id": "g", "members": ["morpheus"], "scene": ""}):
102
+ self.assertNotIn("scene", rp.group_detail(g, self.col, self.idx))
103
+
104
+ def test_anchored_group_is_not_open_cast(self):
105
+ g = {"id": "g", "members": ["morpheus"]}
106
+ self.assertNotIn("open_cast", rp.group_detail(g, self.col, self.idx))
107
+
108
+ def test_open_cast_group_flagged_with_empty_members(self):
109
+ g = {"id": "rebels", "name": "Star Wars Rebels",
110
+ "scene": "Figures from the Rebels universe drop in as the topic calls for them."}
111
+ d = rp.group_detail(g, self.col, self.idx)
112
+ self.assertTrue(d["open_cast"])
113
+ self.assertEqual(d["members"], [])
114
+ self.assertEqual(d["scene"][:7], "Figures")
115
+
116
+ def test_memory_enabled_follows_group_flag_and_defaults_off(self):
117
+ on = rp.group_detail({"id": "g", "members": ["morpheus"], "memory": True}, self.col, self.idx)
118
+ self.assertTrue(on["memory_enabled"])
119
+ off = rp.group_detail({"id": "g", "members": ["morpheus"], "memory": False}, self.col, self.idx)
120
+ self.assertFalse(off["memory_enabled"])
121
+ absent = rp.group_detail({"id": "g", "members": ["morpheus"]}, self.col, self.idx)
122
+ self.assertFalse(absent["memory_enabled"]) # opt-in per named group
123
+
124
+
125
+ class TestInstalledCodesIsDefaultRoom(unittest.TestCase):
126
+ """The default room is installed agents only; pure customs stay in the pool."""
127
+
128
+ def test_pure_custom_excluded_override_kept_in_default_room(self):
129
+ col, _, installed = rp.build_collective(AGENTS, [
130
+ {"code": "morpheus", "name": "Morpheus"}, # pure custom
131
+ {"code": "analyst", "name": "Mary-Custom", "persona": "p"}, # override
132
+ {"code": "sec-hawk", "name": "Vex"}, # shipped crew member
133
+ ])
134
+ # Pure customs are in the pool...
135
+ self.assertIn("morpheus", col)
136
+ self.assertIn("sec-hawk", col)
137
+ # ...but NOT in the default room.
138
+ self.assertEqual(installed, ["bmad-agent-analyst", "bmad-agent-pm"])
139
+ default_room = [col[c]["code"] for c in installed]
140
+ self.assertEqual(default_room, ["bmad-agent-analyst", "bmad-agent-pm"])
141
+ # An override keeps its installed slot (and its custom content).
142
+ self.assertEqual(col["bmad-agent-analyst"]["name"], "Mary-Custom")
143
+
144
+
145
+ if __name__ == "__main__":
146
+ unittest.main()
@@ -18,7 +18,7 @@ Multiple skills may call to update the same spec over time.
18
18
 
19
19
  ## On Activation
20
20
 
21
- 1. Resolve customization: `python3 {project-root}/_bmad/scripts/resolve_customization.py --skill {skill-root} --key workflow`. On failure, read `{skill-root}/customize.toml` directly.
21
+ 1. Resolve customization: `uv run {project-root}/_bmad/scripts/resolve_customization.py --skill {skill-root} --key workflow`. On failure, read `{skill-root}/customize.toml` directly.
22
22
  2. Run `{workflow.activation_steps_prepend}`. Treat `{workflow.persistent_facts}` as foundational context (`file:` entries are loaded).
23
23
  3. Load `{project-root}/_bmad/core/config.yaml` (and `config.user.yaml` if present), root level and `bmm` section. Resolve `{user_name}`, `{communication_language}`, `{document_output_language}`, `{planning_artifacts}`, `{project_name}`, `{date}`.
24
24
  4. Detect mode. **Headless** when any of: no TTY, programmatic caller (another skill or non-interactive runner), or the first message pre-supplies all inputs and asks for an artifact path back. **Interactive** otherwise. In interactive mode, greet by `{user_name}` in `{communication_language}`, stay in that language, and mention that `bmad-party-mode` and `bmad-advanced-elicitation` are available for deeper exploration on any field.
@@ -57,8 +57,8 @@ Deriving the contract from a living log instead of editing the contract in place
57
57
 
58
58
  Writes go through the shared script — `{project-root}/_bmad/scripts/memlog.py`, the same location as `resolve_customization.py` (atomic; never read it back except to resume):
59
59
 
60
- - `python3 {project-root}/_bmad/scripts/memlog.py init --workspace {spec-folder} --field topic="<what is being specced>"` — once, at create.
61
- - `python3 {project-root}/_bmad/scripts/memlog.py append --workspace {spec-folder} --type <decision|constraint|capability|assumption|question|direction|note|event> --text "<one-line gist, reason included>"` — as each lands.
60
+ - `uv run {project-root}/_bmad/scripts/memlog.py init --workspace {spec-folder} --field topic="<what is being specced>"` — once, at create.
61
+ - `uv run {project-root}/_bmad/scripts/memlog.py append --workspace {spec-folder} --type <decision|constraint|capability|assumption|question|direction|note|event> --text "<one-line gist, reason included>"` — as each lands.
62
62
  - Terminal moments (a validation verdict, "spec finalized") are `--type event` entries; the memlog carries no status field.
63
63
 
64
64
  ## The Operation
@@ -69,6 +69,8 @@ When the input is structured and pre-sorted (a PRD with an addendum, a GDD, a br
69
69
 
70
70
  Distill the input into the five-field kernel using `{workflow.spec_template}` as the skeleton. When input is rich, extract directly — no elicitation. When input is sparse, choose: **express** (best-effort distill, every gap becomes an `open_questions[]` entry) or **guided** (walk the five fields with the user one at a time). Headless defaults to express and logs the choice. Interactive asks.
71
71
 
72
+ A recognized domain implication the input leaves unaddressed *is* such a gap — name it as an `open_questions[]` entry (healthcare input silent on PHI/HIPAA, payments silent on PCI, control systems silent on fail-safe) and move on. Flag it; never invent the answer or coach toward it. If these dominate, the input is too thin — suggest `bmad-prd`.
73
+
72
74
  Write lean from the first pass: every sentence must earn its place. Decoration costs tokens and dilutes downstream readers.
73
75
 
74
76
  Log each decision, capability, constraint, and accepted change to `.memlog.md` as it is made — that running record is what the render reads. Because the log is append-only, a later entry supersedes an earlier one on the same point while the history stays intact. When two currently-live sources or companions disagree on the same field, or an either/or never got resolved, surface it to the user rather than silently choosing — the resolution is itself a new memlog entry.
@@ -20,9 +20,9 @@ Name which (or which combination) applies, who is affected, and the backdrop tha
20
20
 
21
21
  ## Capabilities
22
22
 
23
- - id: CAP-1
24
- intent: {One sentence. "User or system can do X to achieve Y." WHAT, not HOW.}
25
- success: {Testable or demonstrable criterion. Something a test or a real demonstration can decide.}
23
+ - **CAP-1**
24
+ - **intent:** {One sentence. "User or system can do X to achieve Y." WHAT, not HOW.}
25
+ - **success:** {Testable or demonstrable criterion. Something a test or a real demonstration can decide.}
26
26
 
27
27
  ## Constraints
28
28
 
@@ -11,3 +11,4 @@ Core,bmad-review-adversarial-general,Adversarial Review,AR,"Use for quality assu
11
11
  Core,bmad-review-edge-case-hunter,Edge Case Hunter Review,ECH,Use alongside adversarial review for orthogonal coverage — method-driven not attitude-driven.,,[path],anytime,,,false,,
12
12
  Core,bmad-spec,Spec,SP,"Use to distill any intent input (brief, PRD, transcript, brain dump, design folder, mixed multi-source) into a succinct, no-fluff SPEC.md contract + companions that downstream work derives from. Locks the WHAT before the HOW. Works for software, game design, research, editorial, policy, business, anything intent-bearing. Validation mode also available.",,[path],anytime,,,false,{output_folder}/specs/spec-{slug},SPEC.md + companion files
13
13
  Core,bmad-customize,BMad Customize,BC,"Use when you want to change how an agent or workflow behaves — add persistent facts, swap templates, insert activation hooks, or customize menus. Scans what's customizable, picks the right scope (agent vs workflow), writes the override to _bmad/custom/, and verifies the merge. No TOML hand-authoring required.",,,anytime,,,false,{project-root}/_bmad/custom,TOML override files
14
+ Core,bmad-forge-idea,Forge Idea,FI,"Use to pressure-test and harden an idea — software, business, creative, research, or life — until it proves out, hardens into something buildable, or dies cheaply. Persona-driven interrogation; optional handoff to bmad-spec or bmad-quick-dev.",,,anytime,,,false,{output_folder}/forge,refined-idea brief (optional)
@@ -10,12 +10,14 @@ Reads from four layers (highest priority last):
10
10
 
11
11
  Outputs merged JSON to stdout. Errors go to stderr.
12
12
 
13
- Requires Python 3.11+ (uses stdlib `tomllib`). No `uv`, no `pip install`,
14
- no virtualenv plain `python3` is sufficient.
15
-
16
- python3 resolve_config.py --project-root /abs/path/to/project
17
- python3 resolve_config.py --project-root ... --key core
18
- python3 resolve_config.py --project-root ... --key agents
13
+ Uses only the Python stdlib (`tomllib`) no third-party dependencies.
14
+ BMad is standardizing on `uv run` to invoke scripts (uv provisions a suitable
15
+ interpreter for you); a plain `python3` on PATH still works during the
16
+ transition. Either runner needs Python 3.11+ for `tomllib`.
17
+
18
+ uv run resolve_config.py --project-root /abs/path/to/project
19
+ uv run resolve_config.py --project-root ... --key core
20
+ uv run resolve_config.py --project-root ... --key agents
19
21
 
20
22
  Merge rules (same as resolve_customization.py):
21
23
  - Scalars: override wins
@@ -11,12 +11,14 @@ Skill name is derived from the basename of the skill directory.
11
11
 
12
12
  Outputs merged JSON to stdout. Errors go to stderr.
13
13
 
14
- Requires Python 3.11+ (uses stdlib `tomllib`). No `uv`, no `pip install`,
15
- no virtualenv plain `python3` is sufficient.
16
-
17
- python3 resolve_customization.py --skill /abs/path/to/skill-dir
18
- python3 resolve_customization.py --skill ... --key agent
19
- python3 resolve_customization.py --skill ... --key agent.menu
14
+ Uses only the Python stdlib (`tomllib`) no third-party dependencies.
15
+ BMad is standardizing on `uv run` to invoke scripts (uv provisions a suitable
16
+ interpreter for you); a plain `python3` on PATH still works during the
17
+ transition. Either runner needs Python 3.11+ for `tomllib`.
18
+
19
+ uv run resolve_customization.py --skill /abs/path/to/skill-dir
20
+ uv run resolve_customization.py --skill ... --key agent
21
+ uv run resolve_customization.py --skill ... --key agent.menu
20
22
 
21
23
  Merge rules (purely structural — no field-name special-casing):
22
24
  - Scalars (string, int, bool, float): override wins
@@ -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';
@@ -1233,6 +1233,9 @@ class Installer {
1233
1233
  ` 1. Launch your AI agent from your project folder`,
1234
1234
  ` 2. Not sure what to do? Invoke the ${color.cyan('bmad-help')} skill and ask it what to do!`,
1235
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
+ '',
1236
1239
  ` Blog, Docs and Guides: ${color.blue('https://bmadcode.com/')}`,
1237
1240
  ` Community: ${color.blue('https://discord.gg/gk8jAdXWmj')}`,
1238
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
+ };