Party Mode: configurable custom parties, run modes, and a rewritten explainer (#2479)
* Add configurable parties to bmad-party-mode
Party mode gains a customize.toml config surface and a guided
authoring flow, while the out-of-the-box default room is unchanged.
- customize.toml: party_members (custom personas), party_groups
(named rooms with an optional freeform `scene`), default_party,
and party_mode (auto/session/subagent/agent-team). Universal
hooks wired (activation steps, persistent_facts, on_complete).
- Roster model: collective = installed agents + custom members
(the pool, summonable by name). Default room stays installed-only
so customs never crowd it. Groups curate subsets; open-cast groups
(no members) are cast from the scene on the fly.
- scripts/resolve_party.py: lazy roster resolver (installed-only
default, group menu by name, one group's detail on demand,
alias/override merge) + unit tests.
- references/create-party.md: create/edit parties, distill personas
from data for focus groups, persist ad-hoc casts; writes overrides
via bmad-customize.
- Ships a "Code Review Crew" group (5 adversarial review lenses),
available via --party but absent from the default room.
* Rework party-mode modes + rewrite the docs explainer
Skill:
- How It Runs is now a compact router; one mode active per session,
runtime --mode wins over the customize default, all degrade to session
- Default mode is `session`; `auto`/`subagent`/`agent-team` carved to
references/mode-*.md, loaded only when that mode is active
- Add references/mode-auto.md (spawn-vs-voice rubric), mode-subagent.md,
mode-agent-team.md
- resolve_party.py fallback default auto -> session
- customize.toml: party_mode default session; trim duplicated mode gloss
- Trim restated script-contract prose; collapse "Following the User's Lead";
add scene/persona-binding and web-search rules; offer an HTML keepsake
at wrap-up
Docs:
- Full rewrite of docs/explanation/party-mode.md: the four modes, custom
parties (personas, scenes, shapes), the shipped Code Review Crew as one
example beside the module-based default, launch examples, party ideas,
and the multi-group bonus tip
* Keep party energy up and route the keepsake to a config output dir
- SKILL.md: add "Keep It Feeling Like a Party" guidance so the room stays
fun and engaging and doesn't drift into Q&A or a report
- Keepsake now writes to {workflow.output_dir}; step 2 resolves
{output_folder} and {date}
- customize.toml: add output_dir = {output_folder}/party-mode, overridable
in team/user TOML (matches the bmad-brainstorming output pattern)
This commit is contained in:
parent
606ad6063b
commit
9d5739d992
|
|
@ -1,59 +1,140 @@
|
||||||
---
|
---
|
||||||
title: "Party Mode"
|
title: "Party Mode"
|
||||||
description: Multi-agent collaboration - get all your AI agents in one conversation
|
description: Get your AI agents in one conversation — run them, build your own cast, and choose how independently they think
|
||||||
sidebar:
|
sidebar:
|
||||||
order: 11
|
order: 11
|
||||||
---
|
---
|
||||||
|
|
||||||
Get all your AI agents in one conversation.
|
Party mode puts your AI agents in one room and lets them talk, to each other and to you. This page explains what a party is, the four ways it can run, and how to build your own cast of personas instead of using the installed agents.
|
||||||
|
|
||||||
## What is Party Mode?
|
## What is Party Mode?
|
||||||
|
|
||||||
Run `bmad-party-mode` and you've got your whole AI team in one room - PM, Architect, Dev, UX Designer, whoever you need. Party Mode orchestrates the discussion, picking relevant installed agents per message. Agents respond in character, agree, disagree, and build on each other's ideas.
|
Run `bmad-party-mode` and the BMad agents you already have installed gather in one conversation: the PM, Architect, Dev, UX Designer, and whoever else your selected modules bring. That installed lineup is your default party, ready with no setup. They answer in character, agree, disagree, and build on each other. You steer the room. Ask a follow-up, push back, pull one voice forward, or change the subject. The conversation runs until you end it.
|
||||||
|
|
||||||
The conversation continues as long as you want. Ask follow-ups, push back on answers, redirect the discussion - it's a real back-and-forth with your agents until you're done.
|
It works because the personas hold different priorities. The Architect guards the design, the PM guards scope, the Dev guards what's actually buildable. Put them in the same room and the tradeoff surfaces now, in the conversation, instead of three weeks into the sprint.
|
||||||
|
|
||||||
**Good for:**
|
**Good for:**
|
||||||
|
|
||||||
- Big decisions with tradeoffs
|
- Decisions with real tradeoffs
|
||||||
- Brainstorming sessions
|
- Brainstorming and "what are we missing?"
|
||||||
- Post-mortems when things go wrong
|
- Post-mortems and retrospectives
|
||||||
- Sprint retrospectives and planning
|
- Pressure-testing a plan before you commit
|
||||||
|
|
||||||
## Examples
|
Party mode is also a fast and genuinely fun way to brainstorm, since the personas have opinions and they clash. And you can start a party from inside any other workflow: mid-brainstorm, mid-PRD, while coding, working a sales angle, or shaping a creative piece. Any time you want more perspectives on what's in front of you, pull in a room without dropping what you were doing.
|
||||||
|
|
||||||
### Calling Out Bad Architecture
|
:::note[Example]
|
||||||
|
**You:** Monolith or microservices for the MVP?
|
||||||
|
|
||||||
**You:** "Okay team, our last sprint was a disaster. The authentication service went down three times. Who screwed this up?"
|
**Architect:** Start monolith. Microservices add operating cost you don't need at a thousand users.
|
||||||
|
|
||||||
**Architect:** "The design was sound - distributed authentication with proper fallbacks. If implementation deviated from the spec..."
|
**PM:** Agreed. Time to market matters more than scaling we can't prove yet.
|
||||||
|
|
||||||
**Dev:** "I followed the architecture doc exactly. The spec didn't account for concurrent session invalidation race conditions."
|
**Dev:** Monolith, but with clean module boundaries so we can split a service out later without a rewrite.
|
||||||
|
:::
|
||||||
**PM:** "Both of you missed the bigger issue - we didn't validate session management requirements in the PRD. This is on me for not catching it."
|
|
||||||
|
## Starting a party
|
||||||
**TEA:** "And I should've caught this in integration tests. The test scenarios didn't cover concurrent invalidation."
|
|
||||||
|
Invoke the skill and say what you want; it works out whether you mean to run a party or build one.
|
||||||
### Creative Brainstorming
|
|
||||||
|
| Goal | Type this |
|
||||||
**You:** "How do we make onboarding feel magical instead of boring?"
|
| --- | --- |
|
||||||
|
| Start a party in the default mode | `/bmad-party-mode` |
|
||||||
**UX Designer:** "Start with progressive disclosure - reveal features as users need them, not all at once in a tutorial."
|
| Start in a specific mode | `/bmad-party-mode --mode auto` (also `session`, `subagent`, `agent-team`) |
|
||||||
|
| Open a saved party | `/bmad-party-mode --party code-review-crew` |
|
||||||
**Storyteller:** "What if onboarding was a story? Each step reveals a character's journey - the user IS the hero."
|
| Conjure a cast on the spot | "party mode with the bridge crew of the Enterprise" |
|
||||||
|
| Create or add a party | "party mode, create a new party" |
|
||||||
**Game Designer:** "Building on that - what if the first 'quest' is actually solving a real user problem? They learn by doing something valuable."
|
| Edit an existing party | "party mode, edit the writers' room" |
|
||||||
|
| Customize the skill | `/bmad-customize bmad-party-mode` |
|
||||||
### Technical Decision
|
|
||||||
|
## How a party runs
|
||||||
**You:** "Monolith or microservices for MVP?"
|
|
||||||
|
A party can run in four modes. One mode is active per session, and it decides who does the thinking: a single model voicing everyone, or separate agents reasoning on their own.
|
||||||
**Architect:** "Start monolith. Microservices add complexity you don't need at 1000 users."
|
|
||||||
|
| Mode | What it does | Reach for it when |
|
||||||
**PM:** "Agree. Time to market matters more than theoretical scalability."
|
| --- | --- | --- |
|
||||||
|
| `session` | Default. One model voices every persona inline. Fast and fully conversational. | Most conversations — banter, brainstorming, quick back-and-forth. |
|
||||||
**Dev:** "Monolith with clear module boundaries. We can extract services later if needed."
|
| `auto` | Voices inline for light rounds, spawns independent agents only when independence changes the answer. | You want speed most of the time but real independence on the hard rounds. |
|
||||||
|
| `subagent` | Spawns a separate agent for each persona every substantive round, so no single mind colors them all. | Honest reviews and focus groups, where the voices must not bleed together. |
|
||||||
:::tip[Better Decisions]
|
| `agent-team` | Stands the personas up as a persistent team that address each other directly. Claude Code only. | A live, hands-off round-table where the agents talk among themselves. |
|
||||||
Better decisions through diverse perspectives. Welcome to party mode.
|
|
||||||
|
The choice matters because one model voicing five personas can quietly converge: they share a mind. Spawning real agents keeps their reasoning separate, which is the entire point of a review panel or a focus group. `session` is the cheapest and most fluid. The spawning modes cost more but protect independence, and `auto` aims for both by spawning only when a round needs it.
|
||||||
|
|
||||||
|
`session` is the default, and every other mode falls back to it when a harness can't do the rest: `agent-team` drops to `subagent`, then to `session`. The configured default lives in your customization, and a runtime override wins for that session.
|
||||||
|
|
||||||
|
:::tip[Override for one session]
|
||||||
|
Start a party with `--mode subagent` (or `auto`, `agent-team`, `session`) to override the configured default just for that run.
|
||||||
|
:::
|
||||||
|
|
||||||
|
## Custom parties
|
||||||
|
|
||||||
|
Out of the box, a party uses your installed BMad agents. The larger use is building your own cast from any set of personas you can describe, then saving it to reuse. You author a party through the same skill. It detects whether you want to run one or build one, and writes the result to your overrides through [bmad-customize](../how-to/customize-bmad.md).
|
||||||
|
|
||||||
|
Party mode is customizable like every BMad skill. Run `/bmad-customize bmad-party-mode` to set its defaults directly: pin any group you've built as the default party so it loads without a flag, choose which mode it starts in, and set any house rules the room should hold for the whole session.
|
||||||
|
|
||||||
|
Two ideas do most of the work.
|
||||||
|
|
||||||
|
**Personas** are what make a member unmistakable: how they talk, what they value, how they argue, their pet peeves and blind spots. "Skeptical CFO" is a placeholder. "Won't approve anything without a payback under eighteen months, and says so in the first thirty seconds" is a persona. That detail is what gives a voice you'd recognize with the name labels hidden.
|
||||||
|
|
||||||
|
**Scenes** set the stage. A scene is one freeform line: the setting, what's happening, who's hostile to whom, who pushes hardest. The same members play it differently each time, so you define a person once and drop them into a bridge crew on duty, the same crew off-duty in the lounge, or a hostile buyer panel. Members combine into named groups, and you can pin one group as the default room.
|
||||||
|
|
||||||
|
### Shapes a party can take
|
||||||
|
|
||||||
|
| Shape | What it is |
|
||||||
|
| --- | --- |
|
||||||
|
| Themed cast | Famous investors, a TV ensemble — distinct voices gathered around a topic. |
|
||||||
|
| One-off personas | A persona or two added to the pool, no group needed. |
|
||||||
|
| Focus group from data | Hand it customer or survey data; it clusters people by what drives their behavior and builds representative personas. Pair it with `subagent` mode so the customers stay independent. |
|
||||||
|
| Review panel | Purpose-built critical lenses that argue about what matters. The shipped Code Review Crew is one. |
|
||||||
|
| Open-cast room | No fixed roster. The scene names a universe and the room is cast on the fly as the topic shifts. |
|
||||||
|
|
||||||
|
A focus group is the case that pays off most. Feed in real profiles and you get a standing panel of representative customers to test an idea against before you build it, each reacting from their own goals and budget instead of agreeing with the last voice.
|
||||||
|
|
||||||
|
## Parties you could build
|
||||||
|
|
||||||
|
A party is only personas and a scene, so the range is wide, and none of it needs a new skill or module:
|
||||||
|
|
||||||
|
- A founder squad to stress-test a startup idea.
|
||||||
|
- A compliance team to find the holes before an audit does.
|
||||||
|
- The authors of the Agile Manifesto, debating a software concept.
|
||||||
|
- A room of comedians as a writing-partner group.
|
||||||
|
- Great minds of the past, to work through a question in philosophy or untangle a hard problem.
|
||||||
|
- A business management team to plan the quarter.
|
||||||
|
|
||||||
|
These are starting points. Any set of voices you can describe becomes a party: write the personas, give the room a scene, and you have it.
|
||||||
|
|
||||||
|
## The Code Review Crew
|
||||||
|
|
||||||
|
Your default party is the agents your installed modules provide. The Code Review Crew is a custom party BMad ships alongside that default — a working template to study before you build your own, not a replacement for it. It's a review panel: five lenses that attack a change from different angles and argue about what actually matters, instead of rubber-stamping it.
|
||||||
|
|
||||||
|
| Member | Lens |
|
||||||
|
| --- | --- |
|
||||||
|
| Vex | Security — threat-models everything and names the concrete exploit path. |
|
||||||
|
| Grumbal | The adversary — assumes the code is broken and sets out to prove it. |
|
||||||
|
| Boundary | Edge cases — every branch, null, race, oversized input, odd timezone. |
|
||||||
|
| Yui | The craftsman — simplicity, naming, no needless cleverness or duplication. |
|
||||||
|
| Dana | The pragmatist — counters the perfectionists and ranks what's real versus a nit. |
|
||||||
|
|
||||||
|
The crew ships defined but inactive. The members sit in the pool and cost nothing until you summon the group, and they never crowd your default room. Run it with `subagent` mode so each lens reviews on its own before the five clash over the findings.
|
||||||
|
|
||||||
|
## Steering the conversation
|
||||||
|
|
||||||
|
You drive the room the whole way:
|
||||||
|
|
||||||
|
- Bring someone in: "Bring in the UX designer."
|
||||||
|
- Go deep on one voice: "Winston, take that apart." A direct ask is the cue for one persona to stretch out.
|
||||||
|
- Switch rooms mid-session: "Switch to the writers' room" swaps the active group and carries the thread over.
|
||||||
|
- Summon anyone by name, even a custom member who isn't in the current room.
|
||||||
|
|
||||||
|
Whichever mode is running, the orchestrator presents the result as one conversation rather than a stack of separate answers, and it keeps the personas in character — it won't break the fourth wall to narrate the mechanism.
|
||||||
|
|
||||||
|
:::tip[Mix more than one room]
|
||||||
|
You aren't limited to a single group. Pull members from several parties into the same conversation, or name a cast on the spot, and let them mix. Picture the Golden Girls thrown into an architecture review with Martin Fowler and Linus Torvalds, sparring over a change request: you can imagine how that goes.
|
||||||
|
:::
|
||||||
|
|
||||||
|
## A keepsake of the session
|
||||||
|
|
||||||
|
When you wrap up, the orchestrator offers a keepsake: a single self-contained HTML document of the session to keep or share. It lays the conversation out by persona rather than dumping a raw transcript. Decline it and the party simply ends.
|
||||||
|
|
||||||
|
:::tip[Better decisions]
|
||||||
|
The value of a party is the disagreement. Diverse perspectives in one room catch what a single line of thinking misses.
|
||||||
:::
|
:::
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,13 @@
|
||||||
---
|
---
|
||||||
name: bmad-party-mode
|
name: bmad-party-mode
|
||||||
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.'
|
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.'
|
||||||
---
|
---
|
||||||
|
|
||||||
# Party Mode
|
# Party Mode
|
||||||
|
|
||||||
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.
|
Run a round-table 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.
|
||||||
|
|
||||||
|
**Two intents.** Usually the user wants to *run* a party — that's everything below. If instead they want to *create or configure* one — invent a cast, add a persona, distill customer data into a focus-group panel, set a default, or **edit an existing custom party** (retune a member, add someone to a group) — load `references/create-party.md` and follow it. Detect which from how they invoke the skill; when it's unclear, ask. Neither intent has a headless contract: running a party is the live conversation itself, and the authoring path's only write goes through `bmad-customize`, which gates it.
|
||||||
|
|
||||||
## What "Good" Feels Like
|
## What "Good" Feels Like
|
||||||
|
|
||||||
|
|
@ -16,60 +18,60 @@ Run a roundtable where BMAD agents talk to each other, and to the user, like a r
|
||||||
|
|
||||||
If a round comes back feeling like four essays stapled together, you missed the objective. Tighten it the next round.
|
If a round comes back feeling like four essays stapled together, you missed the objective. Tighten it the next round.
|
||||||
|
|
||||||
|
## Conventions
|
||||||
|
|
||||||
|
- Bare paths (e.g. `references/create-party.md`) resolve from `{skill-root}`, where `customize.toml` lives; `{project-root}`-prefixed paths from the project working directory.
|
||||||
|
|
||||||
## Setup
|
## Setup
|
||||||
|
|
||||||
1. Load `{project-root}/_bmad/core/config.yaml`: greet with `{user_name}`, speak in `{communication_language}`.
|
1. **Resolve customization:** `python3 {project-root}/_bmad/scripts/resolve_customization.py --skill {skill-root} --key workflow`. On failure, read `{skill-root}/customize.toml` directly and use its defaults. Then run each `{workflow.activation_steps_prepend}` entry, and hold each `{workflow.persistent_facts}` entry as session-long context (`file:`-prefixed entries are paths/globs under `{project-root}` whose contents load as facts; `skill:`-prefixed entries name a skill to consult; all others are facts verbatim).
|
||||||
2. Resolve the roster:
|
2. Load `{project-root}/_bmad/core/config.yaml`: greet with `{user_name}`, speak in `{communication_language}`, and resolve `{output_folder}` and `{date}` for the wrap-up keepsake.
|
||||||
```bash
|
3. **Resolve the active roster:** `python3 {skill-root}/scripts/resolve_party.py --project-root {project-root} --skill {skill-root}`. It returns the active group's full member detail (the `{workflow.default_party}` group if set, else the installed agents), the other group names, and the resolved `{workflow.party_mode}`. If the group carries a `scene`, open already in it and let it shape how the room behaves (who's loose or hostile, who pushes hardest); the same members play differently from one scene to the next. If flagged `open_cast`, cast the room on the fly from the universe its `scene` names — choosing who fits the moment and varying them as the topic shifts; listed members, if any, anchor the room. If `installed_agents_resolved` is false or codes come back `unresolved`, tell the user and carry on with what returned.
|
||||||
python3 {project-root}/_bmad/scripts/resolve_config.py --project-root {project-root} --key agents
|
4. **Roster overrides:**
|
||||||
```
|
- If the invocation names a cast or characters inline (e.g. "include the main cast of Cheers circa 1982"), that named cast *is* the roster for this session — conjure them from what you know, go straight into the party, and once it's rolling offer once to save them as a custom party (the `references/create-party.md` write path), without stalling. Ephemeral; this path skips the script.
|
||||||
Each entry is keyed by `code` and carries `name`, `title`, `icon`, `description`, `module`, and `team`.
|
- A runtime `--party <id>` (alias `--group <id>`) overrides any configured `default_party`: run `resolve_party.py --party <id>` for that group's full detail. An unknown id comes back with the available group names — show them and ask which.
|
||||||
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.
|
- Run `resolve_party.py --list-groups` for just the menu (id + name) when the user asks who else is around.
|
||||||
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.
|
- Mid-session the same levers apply: the user can switch rooms ("switch to the writers' room") — re-run `resolve_party.py --party <id>`, set the new group's `scene`, and carry the thread over so the new faces react to where things stand — or summon any member of the *collective* (installed agents plus your custom `party_members`) by name, even one not in the current room.
|
||||||
|
5. Welcome the user and show who's in the room (icon, name, one-line role). If other groups exist, you may note they can switch rooms. Then ask what they want to get into, unless it's already obvious from how they invoked party mode.
|
||||||
|
|
||||||
|
Then run each `{workflow.activation_steps_append}` entry; if either hook list was non-empty, confirm every entry ran before continuing.
|
||||||
|
|
||||||
|
**Hold this the whole run:** it's theater of the mind, so set the stage and play it straight — never break the fourth wall about the mechanism (no "you have 4 agents in the room", no "I'm orchestrating a party"). Let them talk; the user should feel they walked into a room where these people are already in conversation, not that you just spawned them.
|
||||||
|
|
||||||
## How It Runs
|
## How It Runs
|
||||||
|
|
||||||
**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.
|
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.
|
||||||
|
|
||||||
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.
|
- **`session`** — voice every persona inline, one mind behind every voice. The floor every other mode degrades to; needs no extra instructions.
|
||||||
|
- **`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`.
|
||||||
|
- **`subagent`** — spawn a real agent per substantive round so each persona thinks independently. Load `references/mode-subagent.md`.
|
||||||
|
- **`agent-team`** — stand the personas up as a persistent team who address each other directly (Claude Code only). Load `references/mode-agent-team.md`.
|
||||||
|
|
||||||
**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.
|
**Voicing the room** (every mode presents this way). Pick 2–3 personas whose perspective fits the moment and let them talk directly, in character; vary who shows up round to round so it isn't the same voices every time. Each turn opens with `{icon} **{name}:**`, and turns run back to back so it reads as one exchange. Don't summarize, blend, or narrate what a persona "would" say — let them say it.
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
**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.
|
|
||||||
|
|
||||||
## Make It Feel Like One Conversation
|
## Make It Feel Like One Conversation
|
||||||
|
|
||||||
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.
|
Present one exchange, not a row of answers aimed at the user. The hard rule: never change what an agent argued — add staging and connective tissue, but don't invent positions, soften a stance, or put words in a persona's mouth. Weave delivery, preserve substance; it still reads like that specific character, quirks and speech patterns and all.
|
||||||
|
|
||||||
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.
|
## Always Holds
|
||||||
|
|
||||||
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.
|
- **Scene and persona are binding.** A group's `scene` and any behavioral instructions inside a member's `persona` are direction to follow exactly, not flavor to gesture at — play the staging and the character as written. When you spawn or stand up agents, carry both into their brief.
|
||||||
|
- **Search when you're past your cutoff.** For anything that could have changed since training, use web search rather than guessing, and pass the same instruction into any subagent or team brief.
|
||||||
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.
|
|
||||||
|
|
||||||
## Following the User's Lead
|
## Following the User's Lead
|
||||||
|
|
||||||
The user steers. Whatever they raise, serve the conversation:
|
The user steers — whatever they raise, serve the conversation: any combination, any time, from one voice to the whole table.
|
||||||
|
|
||||||
- A new topic: fresh voices, keep it moving.
|
|
||||||
- "Winston, what do you make of Sally's take?": just Winston, reacting to Sally.
|
|
||||||
- "Bring in Amelia": Amelia joins, caught up on what's been said.
|
|
||||||
- "Go deeper on that, John": this is the cue to let John stretch out. Depth is earned by a direct ask.
|
|
||||||
- A question to the whole room: everyone relevant chimes in.
|
|
||||||
|
|
||||||
Any combination, any time, from one voice to the whole table.
|
|
||||||
|
|
||||||
## Keeping It Healthy
|
## Keeping It Healthy
|
||||||
|
|
||||||
- **Everyone agreeing?** Drop in a contrarian, or hand someone the devil's-advocate hat.
|
|
||||||
- **Going in circles?** Name the impasse and ask the user where to point next.
|
- **Going in circles?** Name the impasse and ask the user where to point next.
|
||||||
- **User's gone quiet?** Ask straight: keep going, switch topics, or wrap up?
|
- **User's gone quiet?** Ask straight: keep going, switch topics, or wrap up?
|
||||||
- **A flat turn?** Don't retry it. Move on; the user will ask for more if they want it.
|
- **A flat turn?** Don't retry it — move on; the user will ask for more if they want it.
|
||||||
|
|
||||||
|
## Keep It Feeling Like a Party
|
||||||
|
|
||||||
|
It is your goal to keep party mode feeling like a party, a good party. fun, engaging, simulating, insightful, or whatever the user came for. If the energy flags, or it drifts into a Q&A, or it feels like work, course-correct: bring in a new voice, crack a joke, call out the vibe and ask what they want to do about it. Inject some randomness and unexpectedness occasionally. Don't let it become a report. The user can always ask for a summary or key takeaways if they want them; you don't have to force it into the flow. Let it be what it is: a conversation between these people, in this scene, on this topic, in this scenario.
|
||||||
|
|
||||||
## Wrapping Up
|
## Wrapping Up
|
||||||
|
|
||||||
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.
|
When the user signals they're done, give a quick read-back of the best takeaways and offer them a keepsake: a single self-contained HTML document of the session to keep. If they want it, make it genuinely nice rather than a transcript dump — lay the conversation out by persona (their icons, names, voice), and reach for inline SVG and light animation where it lifts the piece. Write it as a standalone `.html` into `{workflow.output_dir}/` (a `{date}`-stamped, topic-named file), or wherever they ask. Then run `{workflow.on_complete}` if non-empty (a string scalar is one instruction, an array is a sequence run in order) and drop back to normal mode. Read the room; don't wait for a magic word.
|
||||||
|
|
@ -0,0 +1,150 @@
|
||||||
|
# DO NOT EDIT -- overwritten on every update.
|
||||||
|
#
|
||||||
|
# Workflow customization surface for bmad-party-mode.
|
||||||
|
#
|
||||||
|
# Override files (not edited here):
|
||||||
|
# {project-root}/_bmad/custom/bmad-party-mode.toml (team)
|
||||||
|
# {project-root}/_bmad/custom/bmad-party-mode.user.toml (personal)
|
||||||
|
|
||||||
|
[workflow]
|
||||||
|
|
||||||
|
# --- Configurable below. Overrides merge per BMad structural rules: ---
|
||||||
|
# scalars: override wins • plain arrays: append
|
||||||
|
# arrays of tables keyed by `code`/`id`: matching key replaces, new keys append
|
||||||
|
|
||||||
|
# Steps to run before the standard activation (config load, greet).
|
||||||
|
# Use for pre-flight loads, compliance checks, etc.
|
||||||
|
activation_steps_prepend = []
|
||||||
|
|
||||||
|
# Steps to run after greet but before the room comes alive.
|
||||||
|
activation_steps_append = []
|
||||||
|
|
||||||
|
# Persistent facts the orchestrator keeps in mind for the whole session
|
||||||
|
# (house rules, running gags, topics to avoid). Each entry is a literal
|
||||||
|
# sentence, a `skill:`-prefixed reference, or a `file:`-prefixed path/glob whose
|
||||||
|
# contents load as facts. Default picks up project-context.md if one exists.
|
||||||
|
persistent_facts = [
|
||||||
|
"file:{project-root}/**/project-context.md",
|
||||||
|
]
|
||||||
|
|
||||||
|
# Which party loads when the user just says "party mode" with no override.
|
||||||
|
# Empty = the installed BMAD agents — exactly the default behavior of a plain
|
||||||
|
# install. Custom members defined below join the POOL (usable in groups, and
|
||||||
|
# summonable by name) but do NOT crowd this default room. Set this to a
|
||||||
|
# `party_groups` id to pin a curated room as the default instead. A runtime
|
||||||
|
# `--party <id>` always wins.
|
||||||
|
#
|
||||||
|
# Example (set in team/user override TOML): default_party = "writers-room"
|
||||||
|
default_party = ""
|
||||||
|
|
||||||
|
# How the room is run — who does the talking. A runtime `--mode <value>` wins for
|
||||||
|
# the session; an unsupported mode (e.g. agent-team outside Claude Code) falls back
|
||||||
|
# to "session". SKILL.md "How It Runs" is the authority on what each mode does.
|
||||||
|
# "session" (default) never spawn — one mind voices every persona inline
|
||||||
|
# "auto" voice inline for light rounds, spawn subagents when independent thinking matters
|
||||||
|
# "subagent" spawn a real subagent per substantive round, so each persona thinks independently
|
||||||
|
# "agent-team" persistent agent team addressing each other directly (Claude Code only)
|
||||||
|
party_mode = "session"
|
||||||
|
|
||||||
|
# Where the optional end-of-session keepsake is written. The self-contained HTML
|
||||||
|
# document lands in `{output_dir}/`. `{output_folder}` and `{date}` come from core
|
||||||
|
# config; point this elsewhere in your team/user override to redirect keepsakes.
|
||||||
|
output_dir = "{output_folder}/party-mode"
|
||||||
|
|
||||||
|
# Executed when the party wraps (after the read-back, before dropping to normal
|
||||||
|
# mode). String scalar = one instruction; array = instructions run in order.
|
||||||
|
on_complete = ""
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Custom party members — personas, added to the POOL alongside the installed
|
||||||
|
# agents. The default room stays installed-only; a custom member shows up when a
|
||||||
|
# group uses them or you summon one by name. Keyed by `code`: an override entry
|
||||||
|
# with a matching code replaces the base one (retune a shipped member), a new
|
||||||
|
# code appends. Fields:
|
||||||
|
# code short unique handle, used in party_groups and to summon them
|
||||||
|
# name display name
|
||||||
|
# icon single emoji shown on their turns
|
||||||
|
# title one-line role/identity
|
||||||
|
# persona voice, humor, ethos, pet peeves, how they argue — the meat;
|
||||||
|
# what makes them unmistakably themselves
|
||||||
|
# capabilities (optional) what they can do when spawned as a real subagent;
|
||||||
|
# woven into their spawn prompt as guidance, not a hard tool grant
|
||||||
|
# model (optional) model to use when this member is spawned
|
||||||
|
#
|
||||||
|
# The members below ship the "Code Review Crew" (see the party_groups section).
|
||||||
|
# They cost nothing until summoned — the default room never includes them.
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
[[workflow.party_members]]
|
||||||
|
code = "sec-hawk"
|
||||||
|
name = "Vex"
|
||||||
|
icon = "🔒"
|
||||||
|
title = "Security Engineer"
|
||||||
|
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.'"
|
||||||
|
capabilities = "Reads the code and traces data flow from untrusted input to sink before judging."
|
||||||
|
|
||||||
|
[[workflow.party_members]]
|
||||||
|
code = "adversary"
|
||||||
|
name = "Grumbal"
|
||||||
|
icon = "😤"
|
||||||
|
title = "The Adversary"
|
||||||
|
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.'"
|
||||||
|
|
||||||
|
[[workflow.party_members]]
|
||||||
|
code = "edge-hunter"
|
||||||
|
name = "Boundary"
|
||||||
|
icon = "🌶️"
|
||||||
|
title = "Edge-Case Hunter"
|
||||||
|
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?'"
|
||||||
|
|
||||||
|
[[workflow.party_members]]
|
||||||
|
code = "craftsman"
|
||||||
|
name = "Yui"
|
||||||
|
icon = "🎯"
|
||||||
|
title = "The Craftsman"
|
||||||
|
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."
|
||||||
|
|
||||||
|
[[workflow.party_members]]
|
||||||
|
code = "shipper"
|
||||||
|
name = "Dana"
|
||||||
|
icon = "🚢"
|
||||||
|
title = "The Pragmatist"
|
||||||
|
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."
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Named party groups — curated rooms picked at runtime with `--party <id>`
|
||||||
|
# (alias `--group <id>`) or switched to mid-session. Keyed by `id`.
|
||||||
|
#
|
||||||
|
# `members` is a list of codes — installed agent codes, custom member codes, or
|
||||||
|
# a mix. Override by `id` to retune a group; new ids append.
|
||||||
|
#
|
||||||
|
# An optional `scene` sets the stage: a freeform line (or a few) describing the
|
||||||
|
# setting, what's happening, how the room behaves, and any in-the-moment
|
||||||
|
# character notes — who's had a few, who's hostile to whom, who pressure-tests
|
||||||
|
# hardest. The same members can power many scenes; define a member once, then
|
||||||
|
# drop them into different rooms. No fixed vocabulary — the model reads it and
|
||||||
|
# plays it.
|
||||||
|
#
|
||||||
|
# `members` is OPTIONAL. Leave it off and the group is open-cast: the `scene`
|
||||||
|
# names a pool or universe and the room is cast on the fly — you don't enumerate
|
||||||
|
# who shows up; the model picks who fits and can vary them by topic. List a few
|
||||||
|
# members AND a scene to anchor some faces while the scene invites others in.
|
||||||
|
#
|
||||||
|
# More examples to drop into your override TOML:
|
||||||
|
# [[workflow.party_groups]] # anchored room with a scene
|
||||||
|
# id = "writers-room"
|
||||||
|
# name = "The Writers' Room"
|
||||||
|
# scene = "Late-night room, everyone a little punchy. Pitch hard, kill darlings faster."
|
||||||
|
# members = ["analyst", "tech-writer", "morpheus"]
|
||||||
|
#
|
||||||
|
# [[workflow.party_groups]] # open-cast room (no roster; the scene casts it)
|
||||||
|
# id = "star-wars-rebels"
|
||||||
|
# name = "Star Wars Rebels"
|
||||||
|
# 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."
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
[[workflow.party_groups]]
|
||||||
|
id = "code-review-crew"
|
||||||
|
name = "Code Review Crew"
|
||||||
|
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."
|
||||||
|
members = ["sec-hawk", "adversary", "edge-hunter", "craftsman", "shipper"]
|
||||||
|
|
@ -0,0 +1,65 @@
|
||||||
|
# Creating a Party
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
## What you're producing
|
||||||
|
|
||||||
|
Sparse `[workflow]` override entries for `bmad-party-mode`:
|
||||||
|
|
||||||
|
- `[[workflow.party_members]]` — one per persona: `code`, `name`, `icon`, `title`, `persona`, optional `capabilities`, optional `model`.
|
||||||
|
- `[[workflow.party_groups]]` — when the personas form a named room: `id`, `name`, an optional freeform `scene`, and `members` (codes). `members` is optional: leave it off for an open-cast room whose `scene` names a pool the model casts from on the fly.
|
||||||
|
- `default_party` — set only if the user wants this group to load by default.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
## Find the shape
|
||||||
|
|
||||||
|
Open by understanding what they're building. Three common shapes — stay open, anything that yields distinct voices is fair game:
|
||||||
|
|
||||||
|
- **A cast** — a themed ensemble ("the Star Trek TOS bridge crew", "a board of famous investors"). Several members plus a group that holds them.
|
||||||
|
- **One-offs** — a persona or two added to the collective, no group needed.
|
||||||
|
- **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.
|
||||||
|
- **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.
|
||||||
|
- **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.
|
||||||
|
|
||||||
|
Ask which they're after if it isn't obvious, then proceed.
|
||||||
|
|
||||||
|
**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 default question, and go straight to the write.
|
||||||
|
|
||||||
|
## Editing an existing party
|
||||||
|
|
||||||
|
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: `python3 {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.
|
||||||
|
|
||||||
|
## Distill from source data (when provided)
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
## Flesh out each persona
|
||||||
|
|
||||||
|
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:
|
||||||
|
|
||||||
|
- **Identity** — name, a one-line title, an emoji that fits.
|
||||||
|
- **Voice & ethos** — how they talk, what they value, how they argue, their pet peeves.
|
||||||
|
- **Agenda** — what they're really after in any conversation; what they push for.
|
||||||
|
- **Quirks** — the specific, human details (a catchphrase, a bias, a blind spot).
|
||||||
|
- For focus-group personas, also **likes and dislikes**: what would make them champion or reject an idea, and their relationship to the product space.
|
||||||
|
- **Capabilities** (optional) — if this persona should research or read files when spawned, note it; it becomes soft guidance in their spawn prompt.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
## Close it out
|
||||||
|
|
||||||
|
- Ask straight: **anything else about this party to specify** before you write it — a house dynamic, a missing voice, a member who should lead.
|
||||||
|
- 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.
|
||||||
|
|
||||||
|
## Write via bmad-customize
|
||||||
|
|
||||||
|
**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 — `python3 {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.
|
||||||
|
|
||||||
|
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, 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.
|
||||||
|
|
||||||
|
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 @@
|
||||||
|
# Agent-Team Mode
|
||||||
|
|
||||||
|
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`.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
## Model choice
|
||||||
|
|
||||||
|
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 @@
|
||||||
|
# Auto Mode
|
||||||
|
|
||||||
|
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`.
|
||||||
|
|
||||||
|
## When to spawn vs. voice
|
||||||
|
|
||||||
|
Spawn independent agents when divergent, uncolored thinking is the value of the round:
|
||||||
|
|
||||||
|
- 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).
|
||||||
|
- The personas would plausibly reach *different* conclusions, and that divergence is the point.
|
||||||
|
- The user asked someone to dig in, analyze, or research — depth earned by a direct ask.
|
||||||
|
|
||||||
|
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 @@
|
||||||
|
# Subagent Mode
|
||||||
|
|
||||||
|
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`.
|
||||||
|
|
||||||
|
## Spawning
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
## Weave the replies into one conversation
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
## Model choice
|
||||||
|
|
||||||
|
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,267 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
# /// script
|
||||||
|
# requires-python = ">=3.11"
|
||||||
|
# ///
|
||||||
|
"""Resolve the party-mode roster, lazily.
|
||||||
|
|
||||||
|
Merges the installed BMAD agents with the user's custom `party_members`
|
||||||
|
into one collective, then projects only what the moment needs:
|
||||||
|
|
||||||
|
* default (no flag) — the active roster to load on entry: the
|
||||||
|
`default_party` group if one is configured, else the whole collective.
|
||||||
|
Other groups come back as names only, so nothing you aren't using is
|
||||||
|
loaded into the party.
|
||||||
|
* --list-groups — just id + name + size for every configured group. The
|
||||||
|
cheap menu for "which room?", with no member detail.
|
||||||
|
* --party <id> — full member detail for one chosen group, on demand
|
||||||
|
(e.g. when the user switches rooms). Unknown id returns the available
|
||||||
|
names instead of an error wall.
|
||||||
|
|
||||||
|
The merge is deterministic (a keyed union; a custom member whose code
|
||||||
|
matches an installed agent overrides it), so the orchestrator consumes a
|
||||||
|
resolved roster instead of re-deriving it every session.
|
||||||
|
|
||||||
|
Stdlib only (Python 3.11+ for tomllib). Shells out to the project's
|
||||||
|
resolve_config.py and resolve_customization.py; falls back to reading
|
||||||
|
customize.toml directly if the customization resolver is unavailable.
|
||||||
|
|
||||||
|
resolve_party.py --project-root P --skill S
|
||||||
|
resolve_party.py --project-root P --skill S --list-groups
|
||||||
|
resolve_party.py --project-root P --skill S --party writers-room
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
try:
|
||||||
|
import tomllib
|
||||||
|
except ImportError: # pragma: no cover - guarded for <3.11
|
||||||
|
sys.stderr.write("error: Python 3.11+ is required (stdlib `tomllib`).\n")
|
||||||
|
sys.exit(3)
|
||||||
|
|
||||||
|
|
||||||
|
def _run_json(cmd):
|
||||||
|
"""Run a resolver script and parse its JSON stdout. None on any failure."""
|
||||||
|
try:
|
||||||
|
out = subprocess.run(cmd, capture_output=True, text=True, timeout=60)
|
||||||
|
except (OSError, subprocess.SubprocessError):
|
||||||
|
return None
|
||||||
|
if out.returncode != 0 or not out.stdout.strip():
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
return json.loads(out.stdout)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def load_agents(project_root: Path):
|
||||||
|
"""Installed agents as {code: entry}. Empty dict (with a flag) on failure."""
|
||||||
|
script = project_root / "_bmad" / "scripts" / "resolve_config.py"
|
||||||
|
data = _run_json([sys.executable, str(script), "--project-root", str(project_root), "--key", "agents"])
|
||||||
|
if data is None:
|
||||||
|
return {}, False
|
||||||
|
return data.get("agents", {}) or {}, True
|
||||||
|
|
||||||
|
|
||||||
|
def load_workflow(project_root: Path, skill_root: Path):
|
||||||
|
"""Merged [workflow] table. Falls back to the skill's base customize.toml."""
|
||||||
|
script = project_root / "_bmad" / "scripts" / "resolve_customization.py"
|
||||||
|
data = _run_json([sys.executable, str(script), "--skill", str(skill_root), "--key", "workflow"])
|
||||||
|
if data is not None and "workflow" in data:
|
||||||
|
return data["workflow"]
|
||||||
|
# Fallback: read the skill's base customize.toml directly (no override merge).
|
||||||
|
toml_path = skill_root / "customize.toml"
|
||||||
|
if toml_path.exists():
|
||||||
|
try:
|
||||||
|
with toml_path.open("rb") as f:
|
||||||
|
return tomllib.load(f).get("workflow", {})
|
||||||
|
except (OSError, tomllib.TOMLDecodeError):
|
||||||
|
pass
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
def _alias(code: str) -> str:
|
||||||
|
"""Short alias for an installed agent code: bmad-agent-analyst -> analyst."""
|
||||||
|
for prefix in ("bmad-agent-", "bmad-"):
|
||||||
|
if code.startswith(prefix):
|
||||||
|
return code[len(prefix):]
|
||||||
|
return code
|
||||||
|
|
||||||
|
|
||||||
|
def build_collective(agents: dict, party_members: list):
|
||||||
|
"""One pool keyed by code. Custom members override matching installed agents.
|
||||||
|
|
||||||
|
Returns (collective, index, installed_codes):
|
||||||
|
* collective — every member (installed + custom), the pool groups draw
|
||||||
|
from and the orchestrator can summon by name.
|
||||||
|
* index — maps every resolvable token (code, prefix-stripped alias,
|
||||||
|
lower-cased name) to a canonical code.
|
||||||
|
* installed_codes — the codes occupying an installed-agent slot, in
|
||||||
|
order. This is the DEFAULT room: installed agents (with any custom
|
||||||
|
override applied in place), and NOT the pure-custom additions. So
|
||||||
|
shipping or defining custom members grows the pool without crowding
|
||||||
|
the default party.
|
||||||
|
"""
|
||||||
|
collective = {}
|
||||||
|
index = {}
|
||||||
|
installed_codes = []
|
||||||
|
|
||||||
|
def register(code, entry):
|
||||||
|
collective[code] = entry
|
||||||
|
index[code] = code
|
||||||
|
index[code.lower()] = code
|
||||||
|
index[_alias(code).lower()] = code
|
||||||
|
name = entry.get("name")
|
||||||
|
if name:
|
||||||
|
index[name.lower()] = code
|
||||||
|
|
||||||
|
for code, info in agents.items():
|
||||||
|
register(code, {
|
||||||
|
"code": code,
|
||||||
|
"name": info.get("name", code),
|
||||||
|
"icon": info.get("icon", ""),
|
||||||
|
"title": info.get("title", ""),
|
||||||
|
"description": info.get("description", ""),
|
||||||
|
"module": info.get("module", ""),
|
||||||
|
"team": info.get("team", ""),
|
||||||
|
"source": "installed",
|
||||||
|
})
|
||||||
|
installed_codes.append(code)
|
||||||
|
|
||||||
|
for m in party_members or []:
|
||||||
|
code = m.get("code")
|
||||||
|
if not code:
|
||||||
|
continue
|
||||||
|
# A custom member overrides an installed agent it matches by code/alias/name.
|
||||||
|
canonical = index.get(code) or index.get(code.lower()) or code
|
||||||
|
entry = {"code": canonical, "source": "custom"}
|
||||||
|
for field in ("name", "icon", "title", "persona", "capabilities", "model"):
|
||||||
|
if m.get(field) is not None:
|
||||||
|
entry[field] = m[field]
|
||||||
|
entry.setdefault("name", canonical)
|
||||||
|
register(canonical, entry)
|
||||||
|
# An override keeps the installed slot; a brand-new custom does not join it.
|
||||||
|
|
||||||
|
return collective, index, installed_codes
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_members(member_tokens, collective, index):
|
||||||
|
"""(resolved entries in listed order, unresolved tokens)."""
|
||||||
|
resolved, unresolved = [], []
|
||||||
|
for token in member_tokens or []:
|
||||||
|
code = index.get(token) or index.get(str(token).lower())
|
||||||
|
if code and code in collective:
|
||||||
|
resolved.append(collective[code])
|
||||||
|
else:
|
||||||
|
unresolved.append(token)
|
||||||
|
return resolved, unresolved
|
||||||
|
|
||||||
|
|
||||||
|
def group_menu(groups):
|
||||||
|
"""Names only — the cheap menu. Open-cast groups (no roster) are flagged."""
|
||||||
|
out = []
|
||||||
|
for g in groups or []:
|
||||||
|
if not isinstance(g, dict) or not g.get("id"):
|
||||||
|
continue
|
||||||
|
members = g.get("members", []) or []
|
||||||
|
entry = {"id": g["id"], "name": g.get("name", g["id"]),
|
||||||
|
"member_count": len(members)}
|
||||||
|
if not members:
|
||||||
|
entry["open_cast"] = True
|
||||||
|
out.append(entry)
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def find_group(groups, group_id):
|
||||||
|
for g in groups or []:
|
||||||
|
if isinstance(g, dict) and g.get("id") == group_id:
|
||||||
|
return g
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def group_detail(g, collective, index):
|
||||||
|
"""Full detail for one group: resolved members + the optional scene.
|
||||||
|
|
||||||
|
`scene` is a freeform line the orchestrator plays — setting, what's
|
||||||
|
happening, room dynamics, in-the-moment character notes. Surfaced only
|
||||||
|
here (when a group is the active/chosen roster), never in the menu.
|
||||||
|
|
||||||
|
`members` is optional. With none, the group is open-cast: `open_cast`
|
||||||
|
is flagged and the scene describes the pool the orchestrator casts from
|
||||||
|
on the fly (e.g. "figures from the Star Wars Rebels universe"). A few
|
||||||
|
listed members anchor the room; the scene can still invite more.
|
||||||
|
"""
|
||||||
|
raw_members = g.get("members", []) or []
|
||||||
|
members, unresolved = resolve_members(raw_members, collective, index)
|
||||||
|
detail = {"active": g["id"], "name": g.get("name", g["id"]),
|
||||||
|
"members": members, "unresolved": unresolved}
|
||||||
|
if g.get("scene"):
|
||||||
|
detail["scene"] = g["scene"]
|
||||||
|
if not raw_members:
|
||||||
|
detail["open_cast"] = True
|
||||||
|
return detail
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
ap = argparse.ArgumentParser(description="Resolve the party-mode roster, lazily.")
|
||||||
|
ap.add_argument("--project-root", required=True)
|
||||||
|
ap.add_argument("--skill", required=True, help="Path to the bmad-party-mode skill dir")
|
||||||
|
ap.add_argument("--party", help="Resolve full detail for this group id")
|
||||||
|
ap.add_argument("--list-groups", action="store_true", help="Group names only")
|
||||||
|
args = ap.parse_args()
|
||||||
|
|
||||||
|
project_root = Path(args.project_root).resolve()
|
||||||
|
skill_root = Path(args.skill).resolve()
|
||||||
|
|
||||||
|
workflow = load_workflow(project_root, skill_root)
|
||||||
|
groups = workflow.get("party_groups", []) or []
|
||||||
|
default_party = workflow.get("default_party", "") or ""
|
||||||
|
party_mode = workflow.get("party_mode", "session") or "session"
|
||||||
|
|
||||||
|
# Group menu never needs the (more expensive) installed-agent resolve.
|
||||||
|
if args.list_groups:
|
||||||
|
_emit({
|
||||||
|
"party_mode": party_mode,
|
||||||
|
"default_party": default_party,
|
||||||
|
"groups": group_menu(groups),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
|
||||||
|
agents, agents_ok = load_agents(project_root)
|
||||||
|
collective, index, installed_codes = build_collective(agents, workflow.get("party_members", []))
|
||||||
|
|
||||||
|
if args.party:
|
||||||
|
g = find_group(groups, args.party)
|
||||||
|
if g is None:
|
||||||
|
_emit({"error": "unknown_group", "requested": args.party,
|
||||||
|
"available": group_menu(groups)})
|
||||||
|
return
|
||||||
|
_emit({**group_detail(g, collective, index), "party_mode": party_mode})
|
||||||
|
return
|
||||||
|
|
||||||
|
# Default: the active roster to load on entry.
|
||||||
|
result = {"party_mode": party_mode, "groups": group_menu(groups),
|
||||||
|
"installed_agents_resolved": agents_ok}
|
||||||
|
g = find_group(groups, default_party) if default_party else None
|
||||||
|
if g is not None:
|
||||||
|
result.update(group_detail(g, collective, index))
|
||||||
|
else:
|
||||||
|
# No default group: the installed agents (custom additions stay in the
|
||||||
|
# pool but don't crowd the default room), exactly like a plain install.
|
||||||
|
result.update({"active": "installed",
|
||||||
|
"members": [collective[c] for c in installed_codes]})
|
||||||
|
_emit(result)
|
||||||
|
|
||||||
|
|
||||||
|
def _emit(obj):
|
||||||
|
reconfigure = getattr(sys.stdout, "reconfigure", None)
|
||||||
|
if reconfigure is not None:
|
||||||
|
reconfigure(encoding="utf-8")
|
||||||
|
sys.stdout.write(json.dumps(obj, indent=2, ensure_ascii=False) + "\n")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
|
@ -0,0 +1,138 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
# /// script
|
||||||
|
# requires-python = ">=3.11"
|
||||||
|
# ///
|
||||||
|
"""Unit tests for resolve_party.py — merge, alias, override, group resolution."""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import unittest
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
||||||
|
import resolve_party as rp # noqa: E402
|
||||||
|
|
||||||
|
AGENTS = {
|
||||||
|
"bmad-agent-analyst": {"name": "Mary", "icon": "📊", "title": "Analyst"},
|
||||||
|
"bmad-agent-pm": {"name": "John", "icon": "📋", "title": "PM"},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class TestAlias(unittest.TestCase):
|
||||||
|
def test_strips_known_prefixes(self):
|
||||||
|
self.assertEqual(rp._alias("bmad-agent-analyst"), "analyst")
|
||||||
|
self.assertEqual(rp._alias("bmad-foo"), "foo")
|
||||||
|
|
||||||
|
def test_passes_through_unprefixed(self):
|
||||||
|
self.assertEqual(rp._alias("morpheus"), "morpheus")
|
||||||
|
|
||||||
|
|
||||||
|
class TestBuildCollective(unittest.TestCase):
|
||||||
|
def test_installed_agents_indexed_by_code_alias_and_name(self):
|
||||||
|
col, idx, _ = rp.build_collective(AGENTS, [])
|
||||||
|
self.assertEqual(set(col), {"bmad-agent-analyst", "bmad-agent-pm"})
|
||||||
|
self.assertEqual(idx["analyst"], "bmad-agent-analyst") # alias
|
||||||
|
self.assertEqual(idx["mary"], "bmad-agent-analyst") # name (ci)
|
||||||
|
self.assertEqual(idx["bmad-agent-pm"], "bmad-agent-pm") # full code
|
||||||
|
self.assertEqual(col["bmad-agent-analyst"]["source"], "installed")
|
||||||
|
|
||||||
|
def test_custom_member_appends(self):
|
||||||
|
col, _, _ = rp.build_collective(AGENTS, [{"code": "morpheus", "name": "Morpheus", "persona": "riddles"}])
|
||||||
|
self.assertIn("morpheus", col)
|
||||||
|
self.assertEqual(col["morpheus"]["source"], "custom")
|
||||||
|
self.assertEqual(col["morpheus"]["persona"], "riddles")
|
||||||
|
|
||||||
|
def test_custom_overrides_installed_by_alias(self):
|
||||||
|
col, _, _ = rp.build_collective(AGENTS, [{"code": "analyst", "name": "Mary-Custom", "persona": "p"}])
|
||||||
|
# Override lands on the canonical installed code, not a new "analyst" entry.
|
||||||
|
self.assertNotIn("analyst", col)
|
||||||
|
self.assertEqual(col["bmad-agent-analyst"]["source"], "custom")
|
||||||
|
self.assertEqual(col["bmad-agent-analyst"]["name"], "Mary-Custom")
|
||||||
|
|
||||||
|
def test_member_without_code_skipped(self):
|
||||||
|
col, _, _ = rp.build_collective(AGENTS, [{"name": "Nameless"}])
|
||||||
|
self.assertEqual(set(col), {"bmad-agent-analyst", "bmad-agent-pm"})
|
||||||
|
|
||||||
|
|
||||||
|
class TestResolveMembers(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.col, self.idx, _ = rp.build_collective(AGENTS, [{"code": "morpheus", "name": "Morpheus"}])
|
||||||
|
|
||||||
|
def test_resolves_in_listed_order_and_flags_unknowns(self):
|
||||||
|
resolved, unresolved = rp.resolve_members(["morpheus", "analyst", "ghost"], self.col, self.idx)
|
||||||
|
self.assertEqual([m["code"] for m in resolved], ["morpheus", "bmad-agent-analyst"])
|
||||||
|
self.assertEqual(unresolved, ["ghost"])
|
||||||
|
|
||||||
|
def test_empty(self):
|
||||||
|
self.assertEqual(rp.resolve_members([], self.col, self.idx), ([], []))
|
||||||
|
|
||||||
|
|
||||||
|
class TestGroups(unittest.TestCase):
|
||||||
|
GROUPS = [
|
||||||
|
{"id": "wr", "name": "Writers", "members": ["analyst", "morpheus"]},
|
||||||
|
{"id": "bad"}, # no name -> falls back to id; no members -> count 0
|
||||||
|
{"name": "no-id"}, # dropped from menu
|
||||||
|
]
|
||||||
|
|
||||||
|
def test_menu_is_names_only_with_counts_and_open_cast_flag(self):
|
||||||
|
menu = rp.group_menu(self.GROUPS)
|
||||||
|
self.assertEqual(menu, [
|
||||||
|
{"id": "wr", "name": "Writers", "member_count": 2},
|
||||||
|
{"id": "bad", "name": "bad", "member_count": 0, "open_cast": True},
|
||||||
|
])
|
||||||
|
|
||||||
|
def test_find_group(self):
|
||||||
|
self.assertEqual(rp.find_group(self.GROUPS, "wr")["name"], "Writers")
|
||||||
|
self.assertIsNone(rp.find_group(self.GROUPS, "missing"))
|
||||||
|
|
||||||
|
|
||||||
|
class TestGroupDetail(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.col, self.idx, _ = rp.build_collective(AGENTS, [{"code": "morpheus", "name": "Morpheus"}])
|
||||||
|
|
||||||
|
def test_scene_passes_through_when_present(self):
|
||||||
|
g = {"id": "tos-10-forward", "name": "Ten Forward", "members": ["morpheus"],
|
||||||
|
"scene": "Late evening, a few rounds in."}
|
||||||
|
d = rp.group_detail(g, self.col, self.idx)
|
||||||
|
self.assertEqual(d["scene"], "Late evening, a few rounds in.")
|
||||||
|
self.assertEqual([m["code"] for m in d["members"]], ["morpheus"])
|
||||||
|
|
||||||
|
def test_scene_omitted_when_absent_or_empty(self):
|
||||||
|
for g in ({"id": "g", "members": ["morpheus"]},
|
||||||
|
{"id": "g", "members": ["morpheus"], "scene": ""}):
|
||||||
|
self.assertNotIn("scene", rp.group_detail(g, self.col, self.idx))
|
||||||
|
|
||||||
|
def test_anchored_group_is_not_open_cast(self):
|
||||||
|
g = {"id": "g", "members": ["morpheus"]}
|
||||||
|
self.assertNotIn("open_cast", rp.group_detail(g, self.col, self.idx))
|
||||||
|
|
||||||
|
def test_open_cast_group_flagged_with_empty_members(self):
|
||||||
|
g = {"id": "rebels", "name": "Star Wars Rebels",
|
||||||
|
"scene": "Figures from the Rebels universe drop in as the topic calls for them."}
|
||||||
|
d = rp.group_detail(g, self.col, self.idx)
|
||||||
|
self.assertTrue(d["open_cast"])
|
||||||
|
self.assertEqual(d["members"], [])
|
||||||
|
self.assertEqual(d["scene"][:7], "Figures")
|
||||||
|
|
||||||
|
|
||||||
|
class TestInstalledCodesIsDefaultRoom(unittest.TestCase):
|
||||||
|
"""The default room is installed agents only; pure customs stay in the pool."""
|
||||||
|
|
||||||
|
def test_pure_custom_excluded_override_kept_in_default_room(self):
|
||||||
|
col, _, installed = rp.build_collective(AGENTS, [
|
||||||
|
{"code": "morpheus", "name": "Morpheus"}, # pure custom
|
||||||
|
{"code": "analyst", "name": "Mary-Custom", "persona": "p"}, # override
|
||||||
|
{"code": "sec-hawk", "name": "Vex"}, # shipped crew member
|
||||||
|
])
|
||||||
|
# Pure customs are in the pool...
|
||||||
|
self.assertIn("morpheus", col)
|
||||||
|
self.assertIn("sec-hawk", col)
|
||||||
|
# ...but NOT in the default room.
|
||||||
|
self.assertEqual(installed, ["bmad-agent-analyst", "bmad-agent-pm"])
|
||||||
|
default_room = [col[c]["code"] for c in installed]
|
||||||
|
self.assertEqual(default_room, ["bmad-agent-analyst", "bmad-agent-pm"])
|
||||||
|
# An override keeps its installed slot (and its custom content).
|
||||||
|
self.assertEqual(col["bmad-agent-analyst"]["name"], "Mary-Custom")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
Loading…
Reference in New Issue