bmad-method 6.8.1-next.9 → 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.
- package/.claude-plugin/marketplace.json +9 -3
- package/package.json +10 -4
- package/src/bmm-skills/1-analysis/bmad-prfaq/SKILL.md +2 -2
- package/src/bmm-skills/1-analysis/bmad-product-brief/SKILL.md +8 -8
- package/src/bmm-skills/2-plan-workflows/bmad-prd/SKILL.md +7 -7
- package/src/bmm-skills/2-plan-workflows/bmad-prd/assets/headless-schemas.md +2 -2
- package/src/bmm-skills/2-plan-workflows/bmad-prd/customize.toml +1 -1
- package/src/bmm-skills/2-plan-workflows/bmad-prd/references/headless.md +1 -1
- package/src/bmm-skills/2-plan-workflows/bmad-prd/references/validate.md +1 -1
- package/src/bmm-skills/2-plan-workflows/bmad-ux/SKILL.md +8 -8
- package/src/bmm-skills/2-plan-workflows/bmad-ux/assets/design-directions.md +1 -1
- package/src/bmm-skills/2-plan-workflows/bmad-ux/assets/headless-schemas.md +2 -2
- package/src/bmm-skills/2-plan-workflows/bmad-ux/assets/key-screens.md +4 -4
- package/src/bmm-skills/2-plan-workflows/bmad-ux/customize.toml +1 -1
- package/src/bmm-skills/2-plan-workflows/bmad-ux/references/creative-tools.md +1 -1
- package/src/bmm-skills/2-plan-workflows/bmad-ux/references/headless.md +1 -1
- package/src/bmm-skills/2-plan-workflows/bmad-ux/references/validate.md +2 -2
- package/src/bmm-skills/3-solutioning/bmad-agent-architect/customize.toml +2 -2
- package/src/bmm-skills/3-solutioning/bmad-architecture/SKILL.md +85 -0
- package/src/bmm-skills/3-solutioning/bmad-architecture/assets/spine-template.md +79 -0
- package/src/bmm-skills/3-solutioning/bmad-architecture/customize.toml +100 -0
- package/src/bmm-skills/3-solutioning/bmad-architecture/references/headless.md +26 -0
- package/src/bmm-skills/3-solutioning/bmad-architecture/references/reviewer-gate.md +13 -0
- package/src/bmm-skills/3-solutioning/bmad-architecture/scripts/lint_spine.py +257 -0
- package/src/bmm-skills/3-solutioning/bmad-architecture/scripts/tests/test_lint_spine.py +270 -0
- package/src/bmm-skills/3-solutioning/bmad-create-architecture/SKILL.md +16 -60
- package/src/bmm-skills/4-implementation/bmad-retrospective/SKILL.md +15 -0
- package/src/bmm-skills/4-implementation/bmad-sprint-planning/SKILL.md +20 -1
- package/src/bmm-skills/4-implementation/bmad-sprint-planning/checklist.md +2 -1
- package/src/bmm-skills/4-implementation/bmad-sprint-planning/sprint-status-template.yaml +13 -0
- package/src/bmm-skills/4-implementation/bmad-sprint-status/SKILL.md +13 -0
- package/src/bmm-skills/module-help.csv +2 -2
- package/src/core-skills/bmad-brainstorming/SKILL.md +8 -10
- package/src/core-skills/bmad-brainstorming/references/converge.md +1 -1
- package/src/core-skills/bmad-brainstorming/references/finalize.md +1 -1
- package/src/core-skills/bmad-brainstorming/references/headless.md +4 -4
- package/src/core-skills/bmad-brainstorming/references/in-chat-techniques.md +1 -1
- package/src/core-skills/bmad-brainstorming/references/mode-autonomous.md +1 -1
- package/src/core-skills/bmad-brainstorming/scripts/tests/test_brain.py +2 -2
- package/src/core-skills/bmad-customize/scripts/tests/test_list_customizable_skills.py +1 -1
- package/src/core-skills/bmad-forge-idea/SKILL.md +79 -0
- package/src/core-skills/bmad-forge-idea/customize.toml +42 -0
- package/src/core-skills/bmad-forge-idea/scripts/resolve_personas.py +270 -0
- package/src/core-skills/bmad-forge-idea/scripts/tests/test_resolve_personas.py +138 -0
- package/src/core-skills/bmad-party-mode/SKILL.md +39 -56
- package/src/core-skills/bmad-party-mode/customize.toml +175 -0
- package/src/core-skills/bmad-party-mode/references/create-party.md +70 -0
- package/src/core-skills/bmad-party-mode/references/mode-agent-team.md +11 -0
- package/src/core-skills/bmad-party-mode/references/mode-auto.md +13 -0
- package/src/core-skills/bmad-party-mode/references/mode-subagent.md +19 -0
- package/src/core-skills/bmad-party-mode/references/party-memory.md +51 -0
- package/src/core-skills/bmad-party-mode/scripts/resolve_party.py +272 -0
- package/src/core-skills/bmad-party-mode/scripts/tests/test-resolve_party.py +146 -0
- package/src/core-skills/bmad-spec/SKILL.md +5 -3
- package/src/core-skills/bmad-spec/assets/spec-template.md +3 -3
- package/src/core-skills/module-help.csv +1 -0
- package/src/scripts/resolve_config.py +8 -6
- package/src/scripts/resolve_customization.py +8 -6
- package/tools/installer/commands/install.js +3 -0
- package/tools/installer/core/installer.js +3 -0
- package/tools/installer/core/uv-check.js +97 -0
- package/tools/installer/core/wsl-node-check.js +109 -0
- package/tools/installer/ide/platform-codes.yaml +7 -0
- package/tools/installer/install-messages.yaml +4 -0
- package/tools/installer/ui.js +10 -9
- package/evals/bmm-skills/bmad-product-brief/evals.json +0 -237
- package/evals/bmm-skills/bmad-product-brief/files/branfield-memo.md +0 -46
- package/evals/bmm-skills/bmad-product-brief/files/forkbird-brief/addendum.md +0 -40
- package/evals/bmm-skills/bmad-product-brief/files/forkbird-brief/brief.md +0 -56
- package/evals/bmm-skills/bmad-product-brief/files/forkbird-brief/decision-log.md +0 -27
- package/evals/bmm-skills/bmad-product-brief/files/meridian-mobility-report.md +0 -116
- package/evals/bmm-skills/bmad-product-brief/files/mossridge-brief/addendum.md +0 -41
- package/evals/bmm-skills/bmad-product-brief/files/mossridge-brief/brief.md +0 -57
- package/evals/bmm-skills/bmad-product-brief/files/mossridge-brief/decision-log.md +0 -29
- package/evals/bmm-skills/bmad-product-brief/files/pantry-bridge-interviews.md +0 -90
- package/evals/bmm-skills/bmad-product-brief/files/q2-brainstorm.md +0 -101
- package/evals/bmm-skills/bmad-product-brief/triggers.json +0 -18
- package/src/bmm-skills/3-solutioning/bmad-create-architecture/architecture-decision-template.md +0 -12
- package/src/bmm-skills/3-solutioning/bmad-create-architecture/data/domain-complexity.csv +0 -13
- package/src/bmm-skills/3-solutioning/bmad-create-architecture/data/project-types.csv +0 -7
- package/src/bmm-skills/3-solutioning/bmad-create-architecture/steps/step-01-init.md +0 -153
- package/src/bmm-skills/3-solutioning/bmad-create-architecture/steps/step-01b-continue.md +0 -173
- package/src/bmm-skills/3-solutioning/bmad-create-architecture/steps/step-02-context.md +0 -224
- package/src/bmm-skills/3-solutioning/bmad-create-architecture/steps/step-03-starter.md +0 -329
- package/src/bmm-skills/3-solutioning/bmad-create-architecture/steps/step-04-decisions.md +0 -318
- package/src/bmm-skills/3-solutioning/bmad-create-architecture/steps/step-05-patterns.md +0 -359
- package/src/bmm-skills/3-solutioning/bmad-create-architecture/steps/step-06-structure.md +0 -379
- package/src/bmm-skills/3-solutioning/bmad-create-architecture/steps/step-07-validation.md +0 -361
- package/src/bmm-skills/3-solutioning/bmad-create-architecture/steps/step-08-complete.md +0 -82
- package/src/core-skills/bmad-brainstorming/scripts/memlog.py +0 -202
- package/src/core-skills/bmad-brainstorming/scripts/tests/test_memlog.py +0 -265
- package/tools/installer/core/python-check.js +0 -199
|
@@ -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: `
|
|
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
|
-
- `
|
|
61
|
-
- `
|
|
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
|
-
-
|
|
24
|
-
intent
|
|
25
|
-
success
|
|
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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
+
};
|