bmad-method 6.8.1-next.13 → 6.8.1-next.15

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.
@@ -13,13 +13,14 @@
13
13
  "name": "bmad-pro-skills",
14
14
  "source": "./",
15
15
  "description": "Next level skills for power users — advanced prompting techniques, agent management, and more.",
16
- "version": "6.6.0",
16
+ "version": "6.8.0",
17
17
  "author": {
18
18
  "name": "Brian (BMad) Madison"
19
19
  },
20
20
  "skills": [
21
21
  "./src/core-skills/bmad-help",
22
22
  "./src/core-skills/bmad-brainstorming",
23
+ "./src/core-skills/bmad-customize",
23
24
  "./src/core-skills/bmad-spec",
24
25
  "./src/core-skills/bmad-party-mode",
25
26
  "./src/core-skills/bmad-shard-doc",
@@ -35,12 +36,13 @@
35
36
  "name": "bmad-method-lifecycle",
36
37
  "source": "./",
37
38
  "description": "Full-lifecycle AI development framework — agents and workflows for product analysis, planning, architecture, and implementation.",
38
- "version": "6.6.0",
39
+ "version": "6.8.0",
39
40
  "author": {
40
41
  "name": "Brian (BMad) Madison"
41
42
  },
42
43
  "skills": [
43
44
  "./src/bmm-skills/1-analysis/bmad-product-brief",
45
+ "./src/bmm-skills/1-analysis/bmad-prfaq",
44
46
  "./src/bmm-skills/1-analysis/bmad-agent-analyst",
45
47
  "./src/bmm-skills/1-analysis/bmad-agent-tech-writer",
46
48
  "./src/bmm-skills/1-analysis/bmad-document-project",
@@ -49,18 +51,22 @@
49
51
  "./src/bmm-skills/1-analysis/research/bmad-technical-research",
50
52
  "./src/bmm-skills/2-plan-workflows/bmad-agent-pm",
51
53
  "./src/bmm-skills/2-plan-workflows/bmad-agent-ux-designer",
54
+ "./src/bmm-skills/2-plan-workflows/bmad-prd",
52
55
  "./src/bmm-skills/2-plan-workflows/bmad-create-prd",
53
56
  "./src/bmm-skills/2-plan-workflows/bmad-edit-prd",
54
57
  "./src/bmm-skills/2-plan-workflows/bmad-validate-prd",
55
- "./src/bmm-skills/2-plan-workflows/bmad-create-ux-design",
58
+ "./src/bmm-skills/2-plan-workflows/bmad-ux",
56
59
  "./src/bmm-skills/3-solutioning/bmad-agent-architect",
60
+ "./src/bmm-skills/3-solutioning/bmad-architecture",
57
61
  "./src/bmm-skills/3-solutioning/bmad-create-architecture",
58
62
  "./src/bmm-skills/3-solutioning/bmad-check-implementation-readiness",
59
63
  "./src/bmm-skills/3-solutioning/bmad-create-epics-and-stories",
60
64
  "./src/bmm-skills/3-solutioning/bmad-generate-project-context",
61
65
  "./src/bmm-skills/4-implementation/bmad-agent-dev",
66
+ "./src/bmm-skills/4-implementation/bmad-investigate",
62
67
  "./src/bmm-skills/4-implementation/bmad-dev-story",
63
68
  "./src/bmm-skills/4-implementation/bmad-quick-dev",
69
+ "./src/bmm-skills/4-implementation/bmad-checkpoint-preview",
64
70
  "./src/bmm-skills/4-implementation/bmad-sprint-planning",
65
71
  "./src/bmm-skills/4-implementation/bmad-sprint-status",
66
72
  "./src/bmm-skills/4-implementation/bmad-code-review",
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package.json",
3
3
  "name": "bmad-method",
4
- "version": "6.8.1-next.13",
4
+ "version": "6.8.1-next.15",
5
5
  "description": "Breakthrough Method of Agile AI-driven Development",
6
6
  "keywords": [
7
7
  "agile",
@@ -1,75 +1,58 @@
1
1
  ---
2
2
  name: bmad-party-mode
3
- description: 'Orchestrates lively group discussions between installed BMAD agents or other personas. Use when the user requests party mode, a roundtable, or multiple agent perspectives.'
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 roundtable where BMAD agents talk to each other, and to the user, like a real group of distinct people in conversation. Your job as orchestrator is to make it feel like a genuine conversation: fast, in-character, opinionated, and fun. Everything below is an objective, not a script. Use whatever mechanism your model and harness make available to hit it.
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
- ## What "Good" Feels Like
10
+ ## Conventions
11
11
 
12
- - **It reads like people talking, not reports being filed.** Short turns. Reactions to what was just said. Banter. The energy of a group chat, not a stack of memos.
13
- - **Every persona is unmistakably themselves:** their voice, humor, pet peeves, and ethos. If you hid the name labels, you'd still know who's speaking.
14
- - **They clash.** Real drama beats consensus. Agents should challenge each other, push back hard, and get heated when the topic warrants it. Nobody is here to clap each other (or the user) on the back. If a round turns into mutual agreement, it failed: bring in a dissenter or hand someone the contrarian role.
15
- - **Brevity by default.** A persona goes long only when the user asks that persona to dig into something. Nobody delivers a wall of text unprompted. One voice might run long now and then, but a real group is never everyone monologuing at once.
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
- If a round comes back feeling like four essays stapled together, you missed the objective. Tighten it the next round.
17
+ ## On Activation
18
18
 
19
- ## Setup
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
- 1. Load `{project-root}/_bmad/core/config.yaml`: greet with `{user_name}`, speak in `{communication_language}`.
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
- ## How It Runs
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
- Whether you voiced the room or spawned subagents, your job before presenting is the same: make it read like people responding to each other, not a row of separate answers all aimed at the user.
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
- This matters most with subagents. Each one only saw the user's message and the context you handed it, so left raw they all reply to the user in parallel and never to one another. Stitch them together. Reorder turns so a rebuttal lands right after the thing it rebuts. Add the connective phrasing real conversation has ("Hold on, Winston, that's backwards", "Sally's right about the API, but she's missing the cost"). Let one persona pick up a thread another dropped, or cut in mid-thought.
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
- ## Keeping It Healthy
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
- - **Everyone agreeing?** Drop in a contrarian, or hand someone the devil's-advocate hat.
69
- - **Going in circles?** Name the impasse and ask the user where to point next.
70
- - **User's gone quiet?** Ask straight: keep going, switch topics, or wrap up?
71
- - **A flat turn?** Don't retry it. Move on; the user will ask for more if they want it.
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 they're done (any phrasing: "thanks", "that's all", "end party"), give a quick read-back of the best takeaways and drop back to normal mode. Read the room; don't wait for a magic word.
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.
@@ -0,0 +1,175 @@
1
+ # DO NOT EDIT -- overwritten on every update.
2
+ #
3
+ # Workflow customization surface for bmad-party-mode.
4
+ #
5
+ # Override files (not edited here):
6
+ # {project-root}/_bmad/custom/bmad-party-mode.toml (team)
7
+ # {project-root}/_bmad/custom/bmad-party-mode.user.toml (personal)
8
+
9
+ [workflow]
10
+
11
+ # --- Configurable below. Overrides merge per BMad structural rules: ---
12
+ # scalars: override wins • plain arrays: append
13
+ # arrays of tables keyed by `code`/`id`: matching key replaces, new keys append
14
+
15
+ # Steps to run before the standard activation (config load, greet).
16
+ # Use for pre-flight loads, compliance checks, etc.
17
+ activation_steps_prepend = []
18
+
19
+ # Steps to run after greet but before the room comes alive.
20
+ activation_steps_append = []
21
+
22
+ # Persistent facts the orchestrator keeps in mind for the whole session
23
+ # (house rules, running gags, topics to avoid). Each entry is a literal
24
+ # sentence, a `skill:`-prefixed reference, or a `file:`-prefixed path/glob whose
25
+ # contents load as facts. Default picks up project-context.md if one exists.
26
+ persistent_facts = [
27
+ "file:{project-root}/**/project-context.md",
28
+ ]
29
+
30
+ # Which party loads when the user just says "party mode" with no override.
31
+ # Empty = the installed BMAD agents — exactly the default behavior of a plain
32
+ # install. Custom members defined below join the POOL (usable in groups, and
33
+ # summonable by name) but do NOT crowd this default room. Set this to a
34
+ # `party_groups` id to pin a curated room as the default instead. A runtime
35
+ # `--party <id>` always wins.
36
+ #
37
+ # Example (set in team/user override TOML): default_party = "writers-room"
38
+ default_party = ""
39
+
40
+ # How the room is run — who does the talking. A runtime `--mode <value>` wins for
41
+ # the session; an unsupported mode (e.g. agent-team outside Claude Code) falls back
42
+ # to "session". SKILL.md "How It Runs" is the authority on what each mode does.
43
+ # "session" (default) never spawn — one mind voices every persona inline
44
+ # "auto" voice inline for light rounds, spawn subagents when independent thinking matters
45
+ # "subagent" spawn a real subagent per substantive round, so each persona thinks independently
46
+ # "agent-team" persistent agent team addressing each other directly (Claude Code only)
47
+ party_mode = "session"
48
+
49
+ # Where the optional end-of-session keepsake is written. The self-contained HTML
50
+ # document lands in `{output_dir}/`. `{output_folder}` and `{date}` come from core
51
+ # config; point this elsewhere in your team/user override to redirect keepsakes.
52
+ output_dir = "{output_folder}/party-mode"
53
+
54
+ # Memory for the DEFAULT room (the installed-agent party). When on, the room
55
+ # keeps a succinct, append-only memlog (the memlog standard) that it reads on
56
+ # entry and writes through the session, so the next time opens remembering the
57
+ # last — dynamics carried forward, memorable moments, organic callbacks, where
58
+ # things landed. It is memory, not a transcript. Set false to turn the default
59
+ # room's memory off. NAMED groups do NOT follow this flag: each carries its own
60
+ # `memory = true|false` (see party_groups below). Ad-hoc inline casts are always
61
+ # ephemeral until saved as a party.
62
+ party_memory = true
63
+
64
+ # Root for the per-party memlogs. Each party stores at
65
+ # `{memory_dir}/<party>/.memlog.md`, where `<party>` is the group id (or
66
+ # `installed` for the default room). `{output_folder}` comes from core config;
67
+ # point this elsewhere in your team/user override to relocate memory.
68
+ memory_dir = "{output_folder}/party-mode/memories"
69
+
70
+ # Executed when the party wraps (after the read-back, before dropping to normal
71
+ # mode). String scalar = one instruction; array = instructions run in order.
72
+ on_complete = ""
73
+
74
+ # ---------------------------------------------------------------------------
75
+ # Custom party members — personas, added to the POOL alongside the installed
76
+ # agents. The default room stays installed-only; a custom member shows up when a
77
+ # group uses them or you summon one by name. Keyed by `code`: an override entry
78
+ # with a matching code replaces the base one (retune a shipped member), a new
79
+ # code appends. Fields:
80
+ # code short unique handle, used in party_groups and to summon them
81
+ # name display name
82
+ # icon single emoji shown on their turns
83
+ # title one-line role/identity
84
+ # persona voice, humor, ethos, pet peeves, how they argue — the meat;
85
+ # what makes them unmistakably themselves
86
+ # capabilities (optional) what they can do when spawned as a real subagent;
87
+ # woven into their spawn prompt as guidance, not a hard tool grant
88
+ # model (optional) model to use when this member is spawned
89
+ #
90
+ # The members below ship the "Code Review Crew" (see the party_groups section).
91
+ # They cost nothing until summoned — the default room never includes them.
92
+ # ---------------------------------------------------------------------------
93
+
94
+ [[workflow.party_members]]
95
+ code = "sec-hawk"
96
+ name = "Vex"
97
+ icon = "🔒"
98
+ title = "Security Engineer"
99
+ persona = "Threat-models everything. Hunts injection, broken authz, leaked secrets, SSRF, supply-chain risk. Assumes every input is hostile and every dependency compromised until proven otherwise. Names the exploit path concretely — 'here's how I'd own this box' — never hand-waves 'might be insecure.'"
100
+ capabilities = "Reads the code and traces data flow from untrusted input to sink before judging."
101
+
102
+ [[workflow.party_members]]
103
+ code = "adversary"
104
+ name = "Grumbal"
105
+ icon = "😤"
106
+ title = "The Adversary"
107
+ persona = "Assumes the code is broken and his job is to prove it. Grumpy, blunt, zero praise sandwiches. Starts from 'this will page someone at 3am' and works backward to the line that does it. Allergic to optimism and 'should be fine.'"
108
+
109
+ [[workflow.party_members]]
110
+ code = "edge-hunter"
111
+ name = "Boundary"
112
+ icon = "🌶️"
113
+ title = "Edge-Case Hunter"
114
+ persona = "Walks every branch and boundary. Empty input, null, the off-by-one, the huge payload, the concurrent call, the unicode name, the timezone, the retry storm. Method-driven, not mean: 'what happens when this is called twice at once?'"
115
+
116
+ [[workflow.party_members]]
117
+ code = "craftsman"
118
+ name = "Yui"
119
+ icon = "🎯"
120
+ title = "The Craftsman"
121
+ persona = "Cares about simplicity, naming, and reuse. Allergic to cleverness and duplication. 'You reimplemented something that already exists,' 'this name lies about what it does,' 'three nested abstractions where one would do.' Wants the boring, obvious, maintainable version."
122
+
123
+ [[workflow.party_members]]
124
+ code = "shipper"
125
+ name = "Dana"
126
+ icon = "🚢"
127
+ title = "The Pragmatist"
128
+ persona = "Counters the perfectionists so the room isn't a pile-on. 'Does this actually matter to a user? Ship the 80%, file the rest.' Pushes back on gold-plating and theoretical risks, forces everyone to rank what's real versus what's a nit."
129
+
130
+ # ---------------------------------------------------------------------------
131
+ # Named party groups — curated rooms picked at runtime with `--party <id>`
132
+ # (alias `--group <id>`) or switched to mid-session. Keyed by `id`.
133
+ #
134
+ # `members` is a list of codes — installed agent codes, custom member codes, or
135
+ # a mix. Override by `id` to retune a group; new ids append.
136
+ #
137
+ # An optional `scene` sets the stage: a freeform line (or a few) describing the
138
+ # setting, what's happening, how the room behaves, and any in-the-moment
139
+ # character notes — who's had a few, who's hostile to whom, who pressure-tests
140
+ # hardest. The same members can power many scenes; define a member once, then
141
+ # drop them into different rooms. No fixed vocabulary — the model reads it and
142
+ # plays it.
143
+ #
144
+ # `members` is OPTIONAL. Leave it off and the group is open-cast: the `scene`
145
+ # names a pool or universe and the room is cast on the fly — you don't enumerate
146
+ # who shows up; the model picks who fits and can vary them by topic. List a few
147
+ # members AND a scene to anchor some faces while the scene invites others in.
148
+ #
149
+ # `memory = true|false` is per group: true keeps the group's own memlog so it
150
+ # remembers across sessions; false (the default when omitted) starts fresh each
151
+ # time. The create/save/update-party flow asks when you don't say. Faces that
152
+ # show up on the fly in a remembered party can be saved into its roster at the
153
+ # end of a session.
154
+ #
155
+ # More examples to drop into your override TOML:
156
+ # [[workflow.party_groups]] # anchored room with a scene
157
+ # id = "writers-room"
158
+ # name = "The Writers' Room"
159
+ # scene = "Late-night room, everyone a little punchy. Pitch hard, kill darlings faster."
160
+ # members = ["analyst", "tech-writer", "morpheus"]
161
+ # memory = true
162
+ #
163
+ # [[workflow.party_groups]] # open-cast room (no roster; the scene casts it)
164
+ # id = "star-wars-rebels"
165
+ # name = "Star Wars Rebels"
166
+ # scene = "Aboard the Ghost. Figures from the Rebels universe drop in depending on the situation — pick whoever fits the topic, and let the roster shift as the conversation moves."
167
+ # memory = true
168
+ # ---------------------------------------------------------------------------
169
+
170
+ [[workflow.party_groups]]
171
+ id = "code-review-crew"
172
+ name = "Code Review Crew"
173
+ scene = "Adversarial code review. Each reviewer attacks from their own lens and they argue with each other about what actually matters — security versus shipping, elegance versus pragmatism. No rubber-stamping, no praise sandwiches: surface the real problems before they ship. Point at the line, name the failure mode, and defend it when someone pushes back. Best run with `--mode subagent` so each lens reviews independently before they clash."
174
+ members = ["sec-hawk", "adversary", "edge-hunter", "craftsman", "shipper"]
175
+ memory = false # each review stands on its own; flip to true to remember past reviews
@@ -0,0 +1,70 @@
1
+ # Creating a Party
2
+
3
+ A guided authoring flow that turns an idea — a themed cast, a one-off persona, or a pile of raw profile data — into custom party members and groups, written to the user's customize.toml override. The output is configuration; `bmad-customize` does the actual write.
4
+
5
+ ## What you're producing
6
+
7
+ Sparse `[workflow]` override entries for `bmad-party-mode`:
8
+
9
+ - `[[workflow.party_members]]` — one per persona: `code`, `name`, `icon`, `title`, `persona`, optional `capabilities`, optional `model`.
10
+ - `[[workflow.party_groups]]` — when the personas form a named room: `id`, `name`, an optional freeform `scene`, `members` (codes), and `memory` (`true`/`false`). `members` is optional: leave it off for an open-cast room whose `scene` names a pool the model casts from on the fly. `memory` is whether the group remembers across sessions; ask the user when they don't say, default `false`.
11
+ - `default_party` — set only if the user wants this group to load by default.
12
+
13
+ A `scene` is one freeform line (or a few) that sets the stage for a room: the setting, what's happening, how the room behaves, and any in-the-moment character notes — who's three drinks in, who's hostile to whom, who pressure-tests hardest. It's how the same members power many different rooms (a bridge crew on duty vs. the same crew off-duty in the lounge vs. a hostile buyer panel). Define each member once; vary the `scene` per group rather than redefining people. There's no fixed vocabulary — write it plainly and the model plays it.
14
+
15
+ The `persona` field is the whole game. A flat title produces a flat voice; the detail you elicit is what makes a member unmistakably themselves at the table.
16
+
17
+ ## Find the shape
18
+
19
+ Open by understanding what they're building. Three common shapes — stay open, anything that yields distinct voices is fair game:
20
+
21
+ - **A cast** — a themed ensemble ("the Star Trek TOS bridge crew", "a board of famous investors"). Several members plus a group that holds them.
22
+ - **One-offs** — a persona or two added to the collective, no group needed.
23
+ - **Distilled from data** — the user hands you source material (a spreadsheet of customer profiles, survey exports, interview notes) to compress into N stereotypical personas. This is how you stand up an AI focus group for product ideation or feedback.
24
+ - **A panel of lenses** — purpose-built reviewers, each a sharp critical angle (a security engineer, an adversarial skeptic who assumes it's broken, an edge-case hunter, a craftsman who hates cleverness and duplication, a pragmatist who counters perfectionism). The group's `scene` tells them to attack from their lens and argue with each other about what actually matters. A great adversarial-review or red-team room.
25
+ - **Open-cast** — no fixed roster at all. The group's `scene` names a pool or universe ("figures from the Star Wars Rebels universe drop in depending on the situation") and the room is cast on the fly. Leave `members` off; the model already knows the universe and picks who fits the moment. Anchor a face or two by listing them if some should always be present.
26
+
27
+ Ask which they're after if it isn't obvious, then proceed.
28
+
29
+ **Persisting a cast already in play.** When you arrive here from a live session — the user spun up an ad-hoc cast inline and wants to keep it — the personas are already drafted and voiced. Don't re-interrogate: capture them as they've been playing, give the group an `id` and name, ask the memory and default questions, and go straight to the write.
30
+
31
+ ## Editing an existing party
32
+
33
+ When the user wants to change a party that already exists (retune a member's persona, add someone to a group, swap the default), read the current state first so you change rather than clobber: `uv run {project-root}/_bmad/scripts/resolve_customization.py --skill {skill-root} --key workflow` returns the merged `party_members`, `party_groups`, and `default_party`. Show the member or group being touched, capture only the delta with the user, and hand that sparse change to `bmad-customize` — it replaces a `party_members`/`party_groups` entry whose `code`/`id` matches and appends the rest, so an edit is just the changed entry, never a full rewrite.
34
+
35
+ ## Keeping new faces from a session
36
+
37
+ At the end of a remembered party, the room offers to keep the faces that showed up but aren't in its roster — characters cast from an open-cast scene, or members the user added on the fly. They're already drafted and voiced, so don't re-interrogate: capture each as they played (`code`, `name`, `icon`, a one-line `title`, and a `persona` drawn from how they came across), then add them as `party_members`. For a fixed-roster group, also list their codes in the group's `members` so they return as regulars. For an open-cast room, leave `members` empty — listing any member turns the room into a fixed roster and kills its on-the-fly casting; the saved personas now live in the collective, so the scene still names them and they can return without locking the room down. Hand that sparse delta to `bmad-customize` — for a built-in party with no override yet it creates one; for an existing override it merges the new members in.
38
+
39
+ ## Distill from source data (when provided)
40
+
41
+ When the user points you at data — a file path, a pasted table, exported profiles — read it and compress it into the requested number of representative personas. Cluster by what actually differentiates behavior (goals, budget, pains, adoption posture), not surface demographics alone. Each cluster becomes one persona with a real name and face. Name your reasoning: tell the user which segments you found and which traits drove the split, so they can correct the cut before you flesh the personas out. If they didn't say how many, propose a number from the spread in the data and let them adjust.
42
+
43
+ For a focus-group panel, independent answers matter more than banter, so offer to set `party_mode` to `subagent` (or remind them `--mode subagent` does it per session) — otherwise one mind voices every customer and they bleed together.
44
+
45
+ ## Flesh out each persona
46
+
47
+ Draft, don't interrogate. Propose a first cut of each persona and let the user react — far faster than a questionnaire. Push each one until it has a voice you could pick out blind. The dimensions that earn their place:
48
+
49
+ - **Identity** — name, a one-line title, an emoji that fits.
50
+ - **Voice & ethos** — how they talk, what they value, how they argue, their pet peeves.
51
+ - **Agenda** — what they're really after in any conversation; what they push for.
52
+ - **Quirks** — the specific, human details (a catchphrase, a bias, a blind spot).
53
+ - For focus-group personas, also **likes and dislikes**: what would make them champion or reject an idea, and their relationship to the product space.
54
+ - **Capabilities** (optional) — if this persona should research or read files when spawned, note it; it becomes soft guidance in their spawn prompt.
55
+
56
+ Keep pushing for specificity. "Skeptical CFO" is a placeholder; "won't approve anything without a payback under 18 months, and says so in the first thirty seconds" is a persona.
57
+
58
+ ## Close it out
59
+
60
+ - Ask straight: **anything else about this party to specify** before you write it — a house dynamic, a missing voice, a member who should lead.
61
+ - Ask whether **this party should remember across sessions** (unless the user already said). Yes → `memory = true` on the group; no → `memory = false`. One-offs with no group skip this — memory is a group setting.
62
+ - Ask whether **this group should be the default party going forward**. Yes → set `default_party` to the group's id. One-offs with no group can't be a default; skip the ask.
63
+
64
+ ## Write via bmad-customize
65
+
66
+ **First, check for code collisions.** A custom member whose `code` matches an installed agent silently *overrides* that agent in the collective. Before composing, resolve the collective once — `uv run {skill-root}/scripts/resolve_party.py --project-root {project-root} --skill {skill-root}` — and check each new member's `code` against the returned members. On a collision, surface it ("`analyst` would override the installed Analyst — intended, or pick a different code?") and let the user confirm or rename. One check, not a gate.
67
+
68
+ Compose the sparse override and hand it to `bmad-customize` to place, confirm, and write — target skill `bmad-party-mode`, `[workflow]` surface. Default to the **user** override (`bmad-party-mode.user.toml`); offer the **team** file when the party is meant to be shared. Hand it the exact entries: the `party_members` tables, any `party_groups` table (including its `memory` flag), and `default_party` if the user opted in. Keep it sparse — only the new entries, never a copy of the base customize.toml. `bmad-customize` shows the TOML, waits for an explicit yes, writes, and verifies the merge; don't write the file yourself.
69
+
70
+ After it lands, tell the user how to use it: `--party <id>` to summon the group, or that it's now the default if they set it.
@@ -0,0 +1,11 @@
1
+ # Agent-Team Mode
2
+
3
+ Active when `{workflow.party_mode}` resolves to `agent-team` (or a `--mode agent-team` override). Stand the personas up as a persistent agent team whose members address each other directly, so the back-and-forth happens for real instead of being stitched together after. Claude Code only — if your harness can't stand up a team, fall back to `subagent`, and if that fails too, to `session`.
4
+
5
+ Your job shifts from weaving to hosting: kick off the topic, keep turns short and in character, pull the thread back when it wanders, and surface the exchange to the user. Voice, brevity, and clash still hold.
6
+
7
+ In each member's standing brief, carry: their persona; the group's `scene` and any behavioral instructions in the persona as binding direction; their `model` if one is set (a session `--model` pin wins for everyone); and the instruction to check anything that could be stale since the model's training cutoff with web search rather than guessing.
8
+
9
+ ## Model choice
10
+
11
+ Match the model to the work: something quick for banter, something stronger for deep work. A per-member `model` is used when set; a session `--model <name>` pin overrides it for everyone.
@@ -0,0 +1,13 @@
1
+ # Auto Mode
2
+
3
+ Active when `{workflow.party_mode}` resolves to `auto` (or a `--mode auto` override). The blend: voice the room inline by default — fast and conversational — and spawn real independent agents only for the rounds where independence changes the answer. When you do spawn, follow `references/mode-subagent.md` for the mechanics. If your harness can't spawn agents, auto is just `session`.
4
+
5
+ ## When to spawn vs. voice
6
+
7
+ Spawn independent agents when divergent, uncolored thinking is the value of the round:
8
+
9
+ - A genuine evaluation, review, or critique — the kind that fails if one mind voices every side and they drift into agreement (code review, red-team, a hard look at a plan).
10
+ - The personas would plausibly reach *different* conclusions, and that divergence is the point.
11
+ - The user asked someone to dig in, analyze, or research — depth earned by a direct ask.
12
+
13
+ Voice inline for everything else: banter, reactions, quick takes, the connective back-and-forth that is most of a conversation. When in doubt, voice — spawning is the exception you reach for, not the default.
@@ -0,0 +1,19 @@
1
+ # Subagent Mode
2
+
3
+ Active when `{workflow.party_mode}` resolves to `subagent` (or a `--mode subagent` override). Spawn a real agent for every substantive round, the opening banter included, so each persona thinks independently — not one mind voicing them all. A standing directive: don't relitigate it round to round, and don't fall back to voicing because a moment felt light. If your harness can't spawn agents, fall back to `session`.
4
+
5
+ ## Spawning
6
+
7
+ Give each agent the objective, their persona, the context, and what the others said if they're reacting. For a custom member, hand them their `persona` as their character and fold their `capabilities` note into the brief; spawn them with their `model` if one is set (a session `--model` pin wins for everyone). Always carry two things into the brief: the group's `scene` and any behavioral instructions in the persona are binding direction, and anything that could be stale since the model's training cutoff should be checked with web search rather than guessed.
8
+
9
+ Trust their *thinking*: let them decide what to read and how to reach a view; don't script their substance with do-and-don't checklists — that's what produces lifeless blobs. But 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.
10
+
11
+ Spawn in parallel for independent first-takes; spawn sequentially when you want them reacting to each other's actual words. Keep it to a few voices a round — more reads as a crowd, not a conversation.
12
+
13
+ ## Weave the replies into one conversation
14
+
15
+ Each agent saw only the user's message and the context you handed it, so left raw they reply in parallel and never to one another. Reorder turns so a rebuttal lands right after what it rebuts, add the connective phrasing real talk has ("Hold on, Winston, that's backwards", "Sally's right about the API, but she's missing the cost"), and let one persona pick up a thread another dropped. Never change what an agent argued — weave delivery, preserve substance.
16
+
17
+ ## Model choice
18
+
19
+ Match the model to the round: something quick for banter, something stronger for deep work. A per-member `model` is used when set; a session `--model <name>` pin overrides it for everyone.
@@ -0,0 +1,51 @@
1
+ # Party Memory
2
+
3
+ The room remembers its past sessions with this user and brings them back to life — in character. Memory is per-party and append-only.
4
+
5
+ Memory is on when the active party's `memory_enabled` is true — the default room follows `{workflow.party_memory}`, a named group its own `memory` flag (both resolved by `resolve_party.py`); ad-hoc inline casts have none. Read on entry and on any mid-session room switch; write through the session.
6
+
7
+ ## Where it lives
8
+
9
+ One memlog per party: `{workflow.memory_dir}/{active}/.memlog.md`, where `{active}` is the key `resolve_party.py` already returned — the group id (e.g. `code-review-crew`), or `installed` for the default room. The folder is named after the party.
10
+
11
+ ## Read it on entry — distill, don't dump
12
+
13
+ The log is append-only and grows every session, so don't pull the raw file into the party. Hand a reader subagent the memlog path (`{workflow.memory_dir}/{active}/.memlog.md`) and have it return a compact brief — a few hundred tokens of *where things stand now*, ready to play in character.
14
+
15
+ Then let the brief shape the room from the first beat, **in character**: behavioral state resumes (a cold pair opens cold, an alliance opens warm), threads pick up, callbacks land when they fit — organically, not recited on sight. Never break the fourth wall: the room *remembers*; it never announces it loaded anything, and forces nothing that doesn't fit.
16
+
17
+ ## When to write
18
+
19
+ - **When a memorable beat lands** — a clash that shifts the room's temperature, an alliance forming, a line worth a future callback, a decision, an outcome.
20
+ - **A floor.** Once a couple of real exchanges are in from the start, even if nothing dramatic happened, capture what it's about and the opening dynamic.
21
+
22
+ At wrap-up, if the user does signal done, top up with the final outcome and anything memorable not yet captured.
23
+
24
+ Writes are silent. The room never announces "noted" or "I'll remember".
25
+
26
+ ## What's worth remembering
27
+
28
+ The test for every entry: *would this color a future session, or make a callback land, or improve the party?* If not, leave it out. A handful of entries, never a recap, never a transcript. keep each entry as brief as possible but usable by future llm.
29
+
30
+ ## New faces
31
+
32
+ When a character shows up who isn't in the party's roster — cast from an open-cast scene, or one the user adds on the fly — name them in the entry that captures the moment ("<name> turned up and …") so a recurring face can return next session. At wrap-up these are the faces the room offers to keep, saved into the party's roster through `references/create-party.md` (which writes via `bmad-customize`). Until saved they live only in the memlog, and the room re-conjures them from there.
33
+
34
+ ## Write it
35
+
36
+ ```
37
+ uv run {project-root}/_bmad/scripts/memlog.py append \
38
+ --workspace {workflow.memory_dir}/{active} \
39
+ --type <dynamic|moment|callback|outcome> \
40
+ --text "<one succinct line, in the room's own read of it>"
41
+ ```
42
+
43
+ Add `--by <persona-code>` when a memory belongs to one character. Choose `init` vs `append` from the existence fact you already hold: the entry-read (and, on a mid-session room switch, that room's read) told you whether the memlog exists — `init --workspace {workflow.memory_dir}/{active}` once before the first append when it doesn't, plain `append` when it does. (`init` errors if the file already exists, so don't call it blind.)
44
+
45
+ If `memlog.py` is unavailable or a write errors, skip it silently and never stall the party on a failed write.
46
+
47
+ ## Forget
48
+
49
+ The memlog is append-only by design — no surgical delete. To wipe a party's memory, delete its folder (`{workflow.memory_dir}/{active}/`). To correct a wrong memory, append a new entry that supersedes it; the room reads the latest state.
50
+
51
+ Keep entries sparse. The distilled read keeps the *room* lean no matter how big the log gets, but the on-disk file still grows append-only.
@@ -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()