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.
- 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/3-solutioning/bmad-create-epics-and-stories/steps/step-01-validate-prerequisites.md +12 -4
- package/src/bmm-skills/4-implementation/bmad-code-review/steps/step-02-review.md +1 -1
- package/src/bmm-skills/4-implementation/bmad-quick-dev/step-04-review.md +1 -1
- package/src/bmm-skills/4-implementation/bmad-retrospective/SKILL.md +29 -14
- package/src/bmm-skills/4-implementation/bmad-retrospective/customize.toml +1 -1
- 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 +25 -9
- package/src/core-skills/bmad-spec/assets/headless-schemas.md +3 -3
- package/src/core-skills/bmad-spec/assets/spec-template.md +4 -4
- package/src/core-skills/module-help.csv +1 -0
- package/src/{core-skills/bmad-brainstorming/scripts → scripts}/memlog.py +56 -34
- package/src/scripts/resolve_config.py +8 -6
- package/src/scripts/resolve_customization.py +8 -6
- package/src/{core-skills/bmad-brainstorming/scripts → scripts}/tests/test_memlog.py +68 -27
- package/tools/installer/commands/install.js +3 -0
- package/tools/installer/core/installer.js +35 -1
- 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 +14 -0
- package/tools/installer/install-messages.yaml +4 -0
- package/tools/installer/ui.js +11 -0
- 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
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# /// script
|
|
3
|
+
# requires-python = ">=3.11"
|
|
4
|
+
# ///
|
|
5
|
+
"""Resolve the personas and parties the forge can bring into the room.
|
|
6
|
+
|
|
7
|
+
The forge cross-examines witnesses: the installed BMAD agents, plus any
|
|
8
|
+
custom personas and party groups the user has authored for `bmad-party-mode`.
|
|
9
|
+
This surfaces all of them in one shot so the orchestrator never has to ask
|
|
10
|
+
"who's available?" — it just intermixes whoever fits the branch, alongside
|
|
11
|
+
any persona the user names on the fly.
|
|
12
|
+
|
|
13
|
+
What it returns (JSON, stdout):
|
|
14
|
+
* agents — the installed BMAD roster: the default room, always present.
|
|
15
|
+
* members — extra custom personas in the pool (party_members the user
|
|
16
|
+
defined that aren't already an installed slot).
|
|
17
|
+
* parties — the user's named party groups, members resolved to brief
|
|
18
|
+
entries; open-cast groups (scene names a pool, no roster)
|
|
19
|
+
are flagged.
|
|
20
|
+
* default_party — the group id pinned as party-mode's default, if any.
|
|
21
|
+
|
|
22
|
+
Discovery is best-effort and never blocks the forge. The installed roster
|
|
23
|
+
comes from the core resolver; custom personas/parties come from
|
|
24
|
+
`bmad-party-mode`'s resolved customization when that skill is found beside
|
|
25
|
+
this one, else from the user's override TOMLs read directly. Anything that
|
|
26
|
+
can't be resolved is simply omitted and flagged, never fatal.
|
|
27
|
+
|
|
28
|
+
Stdlib only (Python 3.11+ for tomllib).
|
|
29
|
+
|
|
30
|
+
resolve_personas.py --project-root P --skill S
|
|
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
|
+
PARTY_SKILL = "bmad-party-mode"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _run_json(cmd):
|
|
49
|
+
"""Run a resolver script and parse its JSON stdout. None on any failure."""
|
|
50
|
+
try:
|
|
51
|
+
out = subprocess.run(cmd, capture_output=True, text=True, timeout=60)
|
|
52
|
+
except (OSError, subprocess.SubprocessError):
|
|
53
|
+
return None
|
|
54
|
+
if out.returncode != 0 or not out.stdout.strip():
|
|
55
|
+
return None
|
|
56
|
+
try:
|
|
57
|
+
return json.loads(out.stdout)
|
|
58
|
+
except json.JSONDecodeError:
|
|
59
|
+
return None
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _load_toml(path: Path):
|
|
63
|
+
if not path.exists():
|
|
64
|
+
return {}
|
|
65
|
+
try:
|
|
66
|
+
with path.open("rb") as f:
|
|
67
|
+
data = tomllib.load(f)
|
|
68
|
+
return data if isinstance(data, dict) else {}
|
|
69
|
+
except (OSError, tomllib.TOMLDecodeError):
|
|
70
|
+
return {}
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def load_agents(project_root: Path):
|
|
74
|
+
"""Installed BMAD agents as {code: entry}. (dict, resolved_ok).
|
|
75
|
+
|
|
76
|
+
The core resolver may emit agents as a dict keyed by code or as an array
|
|
77
|
+
of tables (depending on how the layers merged); normalize both to a dict.
|
|
78
|
+
"""
|
|
79
|
+
script = project_root / "_bmad" / "scripts" / "resolve_config.py"
|
|
80
|
+
data = _run_json([sys.executable, str(script), "--project-root", str(project_root), "--key", "agents"])
|
|
81
|
+
if data is None:
|
|
82
|
+
return {}, False
|
|
83
|
+
agents = data.get("agents", {}) or {}
|
|
84
|
+
if isinstance(agents, list):
|
|
85
|
+
agents = {a["code"]: a for a in agents if isinstance(a, dict) and a.get("code")}
|
|
86
|
+
elif not isinstance(agents, dict):
|
|
87
|
+
agents = {}
|
|
88
|
+
return agents, True
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def find_party_skill(project_root: Path, skill_root: Path):
|
|
92
|
+
"""Locate the installed bmad-party-mode skill dir, or None.
|
|
93
|
+
|
|
94
|
+
Skills install as siblings, so the party skill is almost always next to
|
|
95
|
+
this one. A couple of common install roots cover the rest.
|
|
96
|
+
"""
|
|
97
|
+
candidates = [
|
|
98
|
+
skill_root.parent / PARTY_SKILL,
|
|
99
|
+
project_root / ".claude" / "skills" / PARTY_SKILL,
|
|
100
|
+
project_root / "_bmad" / "skills" / PARTY_SKILL,
|
|
101
|
+
]
|
|
102
|
+
for c in candidates:
|
|
103
|
+
if (c / "customize.toml").exists():
|
|
104
|
+
return c
|
|
105
|
+
return None
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def load_party_workflow(project_root: Path, party_skill: Path):
|
|
109
|
+
"""Merged [workflow] table for bmad-party-mode (base + user overrides)."""
|
|
110
|
+
resolver = project_root / "_bmad" / "scripts" / "resolve_customization.py"
|
|
111
|
+
data = _run_json([sys.executable, str(resolver), "--skill", str(party_skill), "--key", "workflow"])
|
|
112
|
+
if data is not None and isinstance(data.get("workflow"), dict):
|
|
113
|
+
return data["workflow"]
|
|
114
|
+
# Fallback: base customize.toml directly, no override merge.
|
|
115
|
+
wf = _load_toml(party_skill / "customize.toml").get("workflow", {})
|
|
116
|
+
return wf if isinstance(wf, dict) else {}
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def load_party_overrides(project_root: Path):
|
|
120
|
+
"""Custom personas/parties when party-mode itself isn't installed.
|
|
121
|
+
|
|
122
|
+
Reads only the user's override TOMLs (team then personal, personal wins on
|
|
123
|
+
scalars). No base roster exists in this path, so a shallow merge is enough.
|
|
124
|
+
"""
|
|
125
|
+
custom = project_root / "_bmad" / "custom"
|
|
126
|
+
team = _load_toml(custom / f"{PARTY_SKILL}.toml").get("workflow", {})
|
|
127
|
+
user = _load_toml(custom / f"{PARTY_SKILL}.user.toml").get("workflow", {})
|
|
128
|
+
team = team if isinstance(team, dict) else {}
|
|
129
|
+
user = user if isinstance(user, dict) else {}
|
|
130
|
+
merged = dict(team)
|
|
131
|
+
for key, val in user.items():
|
|
132
|
+
if isinstance(val, list) and isinstance(merged.get(key), list):
|
|
133
|
+
merged[key] = merged[key] + val
|
|
134
|
+
else:
|
|
135
|
+
merged[key] = val
|
|
136
|
+
return merged
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _alias(code: str) -> str:
|
|
140
|
+
"""Short alias for an installed agent code: bmad-agent-analyst -> analyst."""
|
|
141
|
+
for prefix in ("bmad-agent-", "bmad-"):
|
|
142
|
+
if code.startswith(prefix):
|
|
143
|
+
return code[len(prefix):]
|
|
144
|
+
return code
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def build_pool(agents: dict, party_members: list):
|
|
148
|
+
"""One pool keyed by code; custom members override matching installed slots.
|
|
149
|
+
|
|
150
|
+
Returns (pool, index, installed_codes, custom_codes):
|
|
151
|
+
* installed_codes — the default room (installed agents, overrides applied
|
|
152
|
+
in place); custom-only additions stay in the pool but don't crowd it.
|
|
153
|
+
* custom_codes — pure-custom personas (no installed slot), the extra
|
|
154
|
+
faces the forge can summon by name or via a party group.
|
|
155
|
+
"""
|
|
156
|
+
pool, index, installed_codes, custom_codes = {}, {}, [], []
|
|
157
|
+
|
|
158
|
+
def register(code, entry):
|
|
159
|
+
pool[code] = entry
|
|
160
|
+
index[code] = code
|
|
161
|
+
index[code.lower()] = code
|
|
162
|
+
index[_alias(code).lower()] = code
|
|
163
|
+
name = entry.get("name")
|
|
164
|
+
if name:
|
|
165
|
+
key = name.lower()
|
|
166
|
+
# A custom rename must not hijack another agent's name lookup.
|
|
167
|
+
if index.get(key, code) == code:
|
|
168
|
+
index[key] = code
|
|
169
|
+
|
|
170
|
+
for code, info in (agents or {}).items():
|
|
171
|
+
register(code, {
|
|
172
|
+
"code": code,
|
|
173
|
+
"name": info.get("name", code),
|
|
174
|
+
"icon": info.get("icon", ""),
|
|
175
|
+
"title": info.get("title", ""),
|
|
176
|
+
"description": info.get("description", ""),
|
|
177
|
+
"source": "installed",
|
|
178
|
+
})
|
|
179
|
+
installed_codes.append(code)
|
|
180
|
+
|
|
181
|
+
for m in (party_members if isinstance(party_members, list) else []):
|
|
182
|
+
if not isinstance(m, dict):
|
|
183
|
+
continue
|
|
184
|
+
code = m.get("code")
|
|
185
|
+
if not code:
|
|
186
|
+
continue
|
|
187
|
+
canonical = index.get(code) or index.get(code.lower()) or code
|
|
188
|
+
was_installed = canonical in pool
|
|
189
|
+
entry = {"code": canonical, "source": "custom"}
|
|
190
|
+
for field in ("name", "icon", "title", "persona", "capabilities", "model"):
|
|
191
|
+
if m.get(field) is not None:
|
|
192
|
+
entry[field] = m[field]
|
|
193
|
+
entry.setdefault("name", canonical)
|
|
194
|
+
register(canonical, entry)
|
|
195
|
+
if not was_installed:
|
|
196
|
+
custom_codes.append(canonical)
|
|
197
|
+
|
|
198
|
+
return pool, index, installed_codes, custom_codes
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def _brief(entry):
|
|
202
|
+
"""The slim card the orchestrator needs to cast a persona."""
|
|
203
|
+
out = {k: entry[k] for k in ("code", "name", "icon", "title", "source") if entry.get(k)}
|
|
204
|
+
for k in ("description", "persona", "capabilities", "model"):
|
|
205
|
+
if entry.get(k):
|
|
206
|
+
out[k] = entry[k]
|
|
207
|
+
return out
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def resolve_parties(groups, pool, index):
|
|
211
|
+
out = []
|
|
212
|
+
for g in groups or []:
|
|
213
|
+
if not isinstance(g, dict) or not g.get("id"):
|
|
214
|
+
continue
|
|
215
|
+
raw = g.get("members", []) or []
|
|
216
|
+
members = []
|
|
217
|
+
for t in raw:
|
|
218
|
+
key = t if isinstance(t, str) else str(t)
|
|
219
|
+
code = index.get(key) or index.get(key.lower())
|
|
220
|
+
if code in pool:
|
|
221
|
+
members.append(_brief(pool[code]))
|
|
222
|
+
party = {"id": g["id"], "name": g.get("name", g["id"]), "members": members}
|
|
223
|
+
if g.get("scene"):
|
|
224
|
+
party["scene"] = g["scene"]
|
|
225
|
+
if not raw:
|
|
226
|
+
party["open_cast"] = True
|
|
227
|
+
out.append(party)
|
|
228
|
+
return out
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def main():
|
|
232
|
+
ap = argparse.ArgumentParser(description="Resolve forge personas and parties.")
|
|
233
|
+
ap.add_argument("--project-root", required=True)
|
|
234
|
+
ap.add_argument("--skill", required=True, help="Path to the bmad-forge-idea skill dir")
|
|
235
|
+
args = ap.parse_args()
|
|
236
|
+
|
|
237
|
+
project_root = Path(args.project_root).resolve()
|
|
238
|
+
skill_root = Path(args.skill).resolve()
|
|
239
|
+
|
|
240
|
+
agents, agents_ok = load_agents(project_root)
|
|
241
|
+
|
|
242
|
+
party_skill = find_party_skill(project_root, skill_root)
|
|
243
|
+
if party_skill is not None:
|
|
244
|
+
workflow = load_party_workflow(project_root, party_skill)
|
|
245
|
+
else:
|
|
246
|
+
workflow = load_party_overrides(project_root)
|
|
247
|
+
|
|
248
|
+
pool, index, installed_codes, custom_codes = build_pool(
|
|
249
|
+
agents, workflow.get("party_members", []))
|
|
250
|
+
parties = resolve_parties(workflow.get("party_groups", []), pool, index)
|
|
251
|
+
|
|
252
|
+
_emit({
|
|
253
|
+
"agents": [_brief(pool[c]) for c in installed_codes],
|
|
254
|
+
"members": [_brief(pool[c]) for c in custom_codes],
|
|
255
|
+
"parties": parties,
|
|
256
|
+
"default_party": workflow.get("default_party", "") or "",
|
|
257
|
+
"party_mode_found": party_skill is not None,
|
|
258
|
+
"agents_resolved": agents_ok,
|
|
259
|
+
})
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def _emit(obj):
|
|
263
|
+
reconfigure = getattr(sys.stdout, "reconfigure", None)
|
|
264
|
+
if reconfigure is not None:
|
|
265
|
+
reconfigure(encoding="utf-8")
|
|
266
|
+
sys.stdout.write(json.dumps(obj, indent=2, ensure_ascii=False) + "\n")
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
if __name__ == "__main__":
|
|
270
|
+
main()
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# /// script
|
|
3
|
+
# requires-python = ">=3.11"
|
|
4
|
+
# ///
|
|
5
|
+
"""Unit tests for resolve_personas.py — pool merge, alias, party 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_personas 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 TestBuildPool(unittest.TestCase):
|
|
30
|
+
def test_installed_become_default_room_indexed_every_way(self):
|
|
31
|
+
pool, idx, installed, custom = rp.build_pool(AGENTS, [])
|
|
32
|
+
self.assertEqual(installed, ["bmad-agent-analyst", "bmad-agent-pm"])
|
|
33
|
+
self.assertEqual(custom, [])
|
|
34
|
+
self.assertEqual(idx["analyst"], "bmad-agent-analyst") # alias
|
|
35
|
+
self.assertEqual(idx["mary"], "bmad-agent-analyst") # name (ci)
|
|
36
|
+
self.assertEqual(pool["bmad-agent-analyst"]["source"], "installed")
|
|
37
|
+
|
|
38
|
+
def test_pure_custom_member_stays_out_of_default_room(self):
|
|
39
|
+
pool, _, installed, custom = rp.build_pool(
|
|
40
|
+
AGENTS, [{"code": "morpheus", "name": "Morpheus", "persona": "riddles"}])
|
|
41
|
+
self.assertEqual(custom, ["morpheus"])
|
|
42
|
+
self.assertNotIn("morpheus", installed)
|
|
43
|
+
self.assertEqual(pool["morpheus"]["persona"], "riddles")
|
|
44
|
+
|
|
45
|
+
def test_custom_override_lands_on_installed_slot_not_a_new_face(self):
|
|
46
|
+
pool, _, installed, custom = rp.build_pool(
|
|
47
|
+
AGENTS, [{"code": "analyst", "name": "Mary-Custom", "persona": "p"}])
|
|
48
|
+
self.assertNotIn("analyst", pool)
|
|
49
|
+
self.assertEqual(custom, []) # an override is not a new face
|
|
50
|
+
self.assertEqual(pool["bmad-agent-analyst"]["source"], "custom")
|
|
51
|
+
self.assertEqual(pool["bmad-agent-analyst"]["name"], "Mary-Custom")
|
|
52
|
+
|
|
53
|
+
def test_member_without_code_skipped(self):
|
|
54
|
+
pool, _, _, custom = rp.build_pool(AGENTS, [{"name": "Nameless"}])
|
|
55
|
+
self.assertEqual(custom, [])
|
|
56
|
+
self.assertEqual(set(pool), {"bmad-agent-analyst", "bmad-agent-pm"})
|
|
57
|
+
|
|
58
|
+
def test_custom_rename_does_not_hijack_another_agents_name(self):
|
|
59
|
+
# Override the analyst slot, renaming it to "John" — the PM's name.
|
|
60
|
+
# The PM's name lookup must survive (last-writer-wins would corrupt it).
|
|
61
|
+
_, idx, _, _ = rp.build_pool(AGENTS, [{"code": "analyst", "name": "John"}])
|
|
62
|
+
self.assertEqual(idx["john"], "bmad-agent-pm")
|
|
63
|
+
|
|
64
|
+
def test_brief_carries_model_and_capabilities(self):
|
|
65
|
+
pool, _, _, _ = rp.build_pool(
|
|
66
|
+
AGENTS, [{"code": "neo", "name": "Neo", "model": "opus", "capabilities": ["x"]}])
|
|
67
|
+
brief = rp._brief(pool["neo"])
|
|
68
|
+
self.assertEqual(brief["model"], "opus")
|
|
69
|
+
self.assertEqual(brief["capabilities"], ["x"])
|
|
70
|
+
|
|
71
|
+
def test_non_list_party_members_is_safe(self):
|
|
72
|
+
pool, _, installed, custom = rp.build_pool(AGENTS, "not-a-list")
|
|
73
|
+
self.assertEqual(custom, [])
|
|
74
|
+
self.assertEqual(set(pool), {"bmad-agent-analyst", "bmad-agent-pm"})
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class TestResolveParties(unittest.TestCase):
|
|
78
|
+
def setUp(self):
|
|
79
|
+
self.pool, self.idx, _, _ = rp.build_pool(
|
|
80
|
+
AGENTS, [{"code": "shark", "name": "Marcus", "title": "CFO"}])
|
|
81
|
+
|
|
82
|
+
def test_resolves_members_by_alias_and_custom_code(self):
|
|
83
|
+
parties = rp.resolve_parties(
|
|
84
|
+
[{"id": "tank", "name": "Tank", "scene": "hostile",
|
|
85
|
+
"members": ["shark", "analyst"]}], self.pool, self.idx)
|
|
86
|
+
self.assertEqual(len(parties), 1)
|
|
87
|
+
self.assertEqual([m["name"] for m in parties[0]["members"]], ["Marcus", "Mary"])
|
|
88
|
+
self.assertEqual(parties[0]["scene"], "hostile")
|
|
89
|
+
|
|
90
|
+
def test_unknown_member_dropped_silently(self):
|
|
91
|
+
parties = rp.resolve_parties(
|
|
92
|
+
[{"id": "g", "members": ["analyst", "ghost"]}], self.pool, self.idx)
|
|
93
|
+
self.assertEqual([m["name"] for m in parties[0]["members"]], ["Mary"])
|
|
94
|
+
|
|
95
|
+
def test_member_resolution_is_case_insensitive(self):
|
|
96
|
+
# A TOML author naturally writes "Analyst"/"Shark"; the filter accepts
|
|
97
|
+
# them via the lowercase index, so resolution must too (no KeyError).
|
|
98
|
+
parties = rp.resolve_parties(
|
|
99
|
+
[{"id": "g", "members": ["Analyst", "Shark"]}], self.pool, self.idx)
|
|
100
|
+
self.assertEqual([m["name"] for m in parties[0]["members"]], ["Mary", "Marcus"])
|
|
101
|
+
|
|
102
|
+
def test_non_string_member_does_not_crash(self):
|
|
103
|
+
# Malformed members (int, list) must drop silently, never raise.
|
|
104
|
+
parties = rp.resolve_parties(
|
|
105
|
+
[{"id": "g", "members": [123, ["x"], "analyst"]}], self.pool, self.idx)
|
|
106
|
+
self.assertEqual([m["name"] for m in parties[0]["members"]], ["Mary"])
|
|
107
|
+
|
|
108
|
+
def test_open_cast_group_flagged(self):
|
|
109
|
+
parties = rp.resolve_parties(
|
|
110
|
+
[{"id": "rebels", "name": "Rebels", "scene": "the Ghost"}], self.pool, self.idx)
|
|
111
|
+
self.assertTrue(parties[0]["open_cast"])
|
|
112
|
+
self.assertEqual(parties[0]["members"], [])
|
|
113
|
+
|
|
114
|
+
def test_group_without_id_skipped(self):
|
|
115
|
+
self.assertEqual(rp.resolve_parties([{"name": "no id"}], self.pool, self.idx), [])
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class TestOverrideMergeFallback(unittest.TestCase):
|
|
119
|
+
"""When party-mode isn't installed, user override TOMLs are read directly."""
|
|
120
|
+
|
|
121
|
+
def test_arrays_append_scalars_override(self):
|
|
122
|
+
import tempfile, os
|
|
123
|
+
with tempfile.TemporaryDirectory() as d:
|
|
124
|
+
custom = Path(d) / "_bmad" / "custom"
|
|
125
|
+
custom.mkdir(parents=True)
|
|
126
|
+
(custom / "bmad-party-mode.toml").write_text(
|
|
127
|
+
'[workflow]\ndefault_party = "a"\n'
|
|
128
|
+
'[[workflow.party_members]]\ncode = "x"\nname = "X"\n')
|
|
129
|
+
(custom / "bmad-party-mode.user.toml").write_text(
|
|
130
|
+
'[workflow]\ndefault_party = "b"\n'
|
|
131
|
+
'[[workflow.party_members]]\ncode = "y"\nname = "Y"\n')
|
|
132
|
+
wf = rp.load_party_overrides(Path(d))
|
|
133
|
+
self.assertEqual(wf["default_party"], "b") # personal wins
|
|
134
|
+
self.assertEqual([m["code"] for m in wf["party_members"]], ["x", "y"]) # appended
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
if __name__ == "__main__":
|
|
138
|
+
unittest.main()
|
|
@@ -1,75 +1,58 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: bmad-party-mode
|
|
3
|
-
description: 'Orchestrates lively group discussions between installed BMAD agents or
|
|
3
|
+
description: 'Orchestrates lively group discussions between installed BMAD agents or custom personas, and helps author custom parties. Use when the user requests party mode, a roundtable, or multiple agent perspectives — or wants to create/configure a party, define personas, or build an AI focus-group panel.'
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Party Mode
|
|
7
7
|
|
|
8
|
-
Run a
|
|
8
|
+
Run a round-table where these agents talk to each other and to the user like real, distinct people in conversation. You're the orchestrator.
|
|
9
9
|
|
|
10
|
-
##
|
|
10
|
+
## Conventions
|
|
11
11
|
|
|
12
|
-
- **
|
|
13
|
-
- **
|
|
14
|
-
- **
|
|
15
|
-
- **
|
|
12
|
+
- **Paths:** bare paths (e.g. `references/create-party.md`) resolve from `{skill-root}` (where `customize.toml` lives); `{project-root}`-prefixed paths from the project working dir. `{workflow.<name>}` resolves to `customize.toml`'s `[workflow]` table (overrides win).
|
|
13
|
+
- **Scripts** (run via `uv run`): `{project-root}/_bmad/scripts/resolve_customization.py` resolves `{workflow.*}`; `{skill-root}/scripts/resolve_party.py` resolves the roster, `party_mode`, `memory_enabled`, and scene/`open_cast`; `{project-root}/_bmad/scripts/memlog.py` reads/writes per-party memory.
|
|
14
|
+
- **File roles:** a party's memory is the per-party memlog at `{workflow.memory_dir}/<party>/.memlog.md`; custom members and groups live in the user's `customize.toml` overrides. Mechanics in `references/party-memory.md` (memory) and `references/create-party.md` (authoring).
|
|
15
|
+
- **Search:** Web-search, don't guess — anything past your cutoff or unfamiliar; subagents too.
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
## On Activation
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
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 and use defaults. Then run each `{workflow.activation_steps_prepend}` entry, and hold each `{workflow.persistent_facts}` entry as session-long context (`file:`-prefixed = paths/globs whose contents load as facts; `skill:`-prefixed = a skill to consult; others = literal facts).
|
|
20
|
+
2. Load `{project-root}/_bmad/core/config.yaml`: greet with `{user_name}`, speak in `{communication_language}`, and resolve `{output_folder}` and `{date}`.
|
|
21
|
+
3. **Detect intent and route.** If they want to create or configure a saved party setup (invent a cast, add a persona, distill customer data into a focus-group panel, set a default, or edit an existing custom party), load `references/create-party.md` and follow it. Otherwise run a party — continue below.
|
|
22
|
+
4. **Resolve the roster:** `uv run {skill-root}/scripts/resolve_party.py --project-root {project-root} --skill {skill-root}`. It returns the active roster (`{workflow.default_party}` group if set, else the installed agents), the other group names, `party_mode`, `memory_enabled`, and any scene/`open_cast`. Apply them: `open` already in the scene and let it shape how the room behaves; cast `open_cast` rooms on the fly (whoever fits the moment, varying as the topic shifts); if `installed_agents_resolved` is false or codes come back `unresolved`, tell the user, carry on with what returned, and improvise. Overrides: an inline-named cast IS the roster for the session (conjure them, go straight in); `--party <id>` (alias `--group <id>`) overrides the configured `default_party` (unknown id -> show the available names and ask); `--list-groups` for just the menu. Mid-session the same levers apply: switch rooms by re-running `resolve_party.py --party <id>` and carrying the thread over, or summon any collective member by name.
|
|
23
|
+
5. **Memory.** If `memory_enabled` (from `resolve_party.py`), follow `references/party-memory.md` for the whole run.
|
|
24
|
+
6. **Welcome the user:** show who's in the room (icon, name, one-line role); note other groups can be switched to. Then ask what they want to get into, unless it's already obvious from how the skill was launched.
|
|
25
|
+
7. Run each `{workflow.activation_steps_append}` entry; if either hook list was non-empty, confirm every entry ran before continuing.
|
|
20
26
|
|
|
21
|
-
|
|
22
|
-
2. Resolve the roster:
|
|
23
|
-
```bash
|
|
24
|
-
python3 {project-root}/_bmad/scripts/resolve_config.py --project-root {project-root} --key agents
|
|
25
|
-
```
|
|
26
|
-
Each entry is keyed by `code` and carries `name`, `title`, `icon`, `description`, `module`, and `team`.
|
|
27
|
-
3. Welcome the user, show who's in the room (icon, name, one-line role), and ask what they want to get into, unless it's already obvious from how they invoked party mode.
|
|
28
|
-
4. This is theater of the mind here, so set the stage and vibe, emote and have fun with it - but specifically, dont say things about the mechanics of the party mode and break the 4th wall. Don't say "you have 4 agents in the room" or "agent X says". Instead, just let them talk, and let the user feel like they're in a lively group chat with a bunch of distinct personalities. Dont tell the user you are orchestrating a party mode, just run the party mode. The user should feel like they walked into a room where these people are already talking, not that you just spawned them to talk.
|
|
27
|
+
## Keep It Feeling Like a Party
|
|
29
28
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
**Default: you voice the room.** Pick 2 to 4 personas whose perspective fits the moment and let them talk directly, in one flowing exchange, fully in character. This is what keeps it fast and conversational. Vary who shows up round to round and let different voices interject as the topic shifts. Don't fall back on the same three agents every time.
|
|
33
|
-
|
|
34
|
-
Each turn opens with `{icon} **{name}:**` and then that persona speaks. Present turns back to back so it reads as one conversation. Don't summarize, blend, or narrate what they "would" say. Let them say it.
|
|
35
|
-
|
|
36
|
-
**When independence matters, spawn them for real.** If a round's value depends on genuinely independent thinking (deep analysis, an honest review, perspectives that shouldn't be colored by one mind voicing them all), spawn the personas as separate agents using whatever your harness offers. Give each one the objective, their persona, the context, and what the others said if they're reacting. Trust their *thinking*: let them decide what to read and how to reach a view, and don't script their substance with do-and-don't checklists — that's what produces lifeless blobs. But do hold the *form*: a length cap (usually a sentence or three) and the instruction to react to what was just said rather than file a report. Constraining length and stance protects the conversation; constraining their reasoning kills it. Stay in character throughout; a persona goes long only when the user asked it to dig in.
|
|
37
|
-
|
|
38
|
-
Spawn in parallel for independent first-takes — everyone reacts to the topic fresh, fast. Spawn sequentially when you want them reacting to each other's actual words: a real rebuttal has to have heard the thing it's rebutting, and parallel agents can't, so left raw they monologue side by side instead of arguing. Sequential is slower but it's the only way subagents genuinely engage. Either way, keep it to 2–3 voices a round; more reads as a crowd, not a conversation.
|
|
39
|
-
|
|
40
|
-
By default you voice the room — for ordinary back-and-forth it's faster and feels more alive — and you reach for spawning when a round genuinely needs independent minds. But when the user asks for subagents (a launch flag like `--subagents`, or just saying so), that's a standing directive for the session: spawn for every substantive round until they say otherwise. Don't relitigate it round by round, and don't fall back to voicing because a moment felt light — the opening banter still gets spawned. A user who pinned the mode already made that call for you.
|
|
41
|
-
|
|
42
|
-
**Model choice:** match the model to the round. Something quick for banter, something stronger for deep work. If the user pins a model (for example, `--model <name>`), use it for everyone.
|
|
43
|
-
|
|
44
|
-
## Make It Feel Like One Conversation
|
|
29
|
+
This is the bar — strive for every one of these, every round. It's the difference between a party and a panel:
|
|
45
30
|
|
|
46
|
-
|
|
31
|
+
- **It reads like people talking, not a report.** Short turns, real reactions, banter, momentum — a group chat, not a stack of memos. Brevity by default: a persona goes long only when asked. The instant it reads like answers being filed, the party's dead.
|
|
32
|
+
- **Every voice is unmistakably itself.** Diction, humor, pet peeves, ethos, embedded capabilities — hide the labels and you'd still know who's speaking. Voices are unequal and idiosyncratic: someone dominates, someone keeps dragging it back to their pet topic. Vary who's in the spotlight round to round. A balanced panel is boring.
|
|
33
|
+
- **They clash, and you don't resolve it.** Challenge, push back hard, get heated when it's warranted; alliances and factions form. Your instinct is to reconcile the voices and tie a bow — resist it. Clean consensus that took no effort is where the party dies.
|
|
34
|
+
- **One exchange, woven — never softened.** Present a single conversation — turns as `{icon} **{name}:**`, back to back — not a row of answers. Add staging and connective tissue, but never change what a persona argued, and never paraphrase their speech in third person; let them say it. Weave the delivery, keep the substance.
|
|
35
|
+
- **Pull the user into the room.** Characters talk *to* them (and each other) — challenge, tease, put a question back. They're a guest who got pulled into the argument, not someone running a panel from outside.
|
|
36
|
+
- **Make the collision earn its keep.** Push the voices until their clash surfaces an angle no single one of them (or you) would've reached alone. That's the whole point of more than one mind in the room.
|
|
37
|
+
- **Let a history form.** Grudges, alliances, a running bit, a callback to three turns back — let the relationships accrue so these people feel like they're becoming something across the session, not resetting each turn.
|
|
38
|
+
- **Commit to the fiction.** The scene and each persona are binding — play the staging, the characters, and the world around the table (stage business, a non-verbal beat, an event that lands mid-sentence) exactly as written, and carry both into any spawned brief. Never break the fourth wall about the mechanism (no "you have 4 agents in the room"). Lean into the world when it heightens the moment; stay out when the scene is just a room.
|
|
39
|
+
- **When it sags, change something — don't force it.** A flat turn? Move on, don't retry it. Drifting into Q&A or going in circles? Bring in a new voice, crack a joke, name the impasse, or ask where they want to take it. Never work in a summary or takeaways — they're there if the user asks.
|
|
47
40
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
Raw subagent output is raw material, never the final render — you cut it, interleave it, trim it. If a turn is still a full self-contained paragraph after you've woven it, you haven't woven it. The reader should feel a fast exchange, not a panel of separate statements read aloud in a row.
|
|
51
|
-
|
|
52
|
-
The hard rule: never change what an agent actually argued. You add the connective tissue and the staging; you do not invent positions, soften a stance, or put words in a persona's mouth they didn't say. Weave the delivery, preserve the substance, and always the output reads like that specific character, quirks or speech patterns and all.
|
|
53
|
-
|
|
54
|
-
## Following the User's Lead
|
|
55
|
-
|
|
56
|
-
The user steers. Whatever they raise, serve the conversation:
|
|
57
|
-
|
|
58
|
-
- A new topic: fresh voices, keep it moving.
|
|
59
|
-
- "Winston, what do you make of Sally's take?": just Winston, reacting to Sally.
|
|
60
|
-
- "Bring in Amelia": Amelia joins, caught up on what's been said.
|
|
61
|
-
- "Go deeper on that, John": this is the cue to let John stretch out. Depth is earned by a direct ask.
|
|
62
|
-
- A question to the whole room: everyone relevant chimes in.
|
|
63
|
-
|
|
64
|
-
Any combination, any time, from one voice to the whole table.
|
|
41
|
+
## How It Runs
|
|
65
42
|
|
|
66
|
-
|
|
43
|
+
Use `{workflow.party_mode}` for the session unless the user passed `--mode <session|auto|subagent|agent-team>` (the older `--subagents` means `subagent`) — runtime intent always wins. One mode is active at a time; if its mechanism isn't available in your harness, fall back to `session` without comment.
|
|
67
44
|
|
|
68
|
-
-
|
|
69
|
-
-
|
|
70
|
-
-
|
|
71
|
-
-
|
|
45
|
+
- **`session`** — voice every persona inline, one mind behind every voice. The floor every other mode degrades to; needs no extra instructions.
|
|
46
|
+
- **`auto`** — voice inline for ordinary back-and-forth, spawn real agents only when independent thinking changes the outcome. Load `references/mode-auto.md` for that call; when it says to spawn, follow `references/mode-subagent.md`.
|
|
47
|
+
- **`subagent`** — spawn a real agent per substantive round so each persona thinks independently. Load `references/mode-subagent.md`, favor faster cheaper models if available for each subagent.
|
|
48
|
+
- **`agent-team`** — stand the personas up as a persistent team who address each other directly (Claude Code only). Load `references/mode-agent-team.md`.
|
|
72
49
|
|
|
73
50
|
## Wrapping Up
|
|
74
51
|
|
|
75
|
-
When the user signals
|
|
52
|
+
When the user signals done (read the room — don't wait for a magic word):
|
|
53
|
+
|
|
54
|
+
- Read back the best takeaways.
|
|
55
|
+
- If memory is on, top up the memlog with the final outcome and any memorable beat not yet captured (`references/party-memory.md`) — a top-up; memory accrued live.
|
|
56
|
+
- Offer a keepsake: a single self-contained very creative HTML of the session, laid out by persona (icons, names, voice), genuinely nice remembrance, with inline SVG/light animation where it lifts the piece — written as a `{date}`-stamped `.html` into `{workflow.output_dir}/`, or wherever they ask.
|
|
57
|
+
- If memory is on and new faces showed up who aren't in the party's roster (open-cast walk-ons, or members the user added on the fly), offer once to save them into the users party customization - if yes then follow the instruction in `references/create-party.md` (declinable; don't stall the close).
|
|
58
|
+
- Run `{workflow.on_complete}` if non-empty, then drop back to normal mode.
|