diff --git a/docs/explanation/party-mode.md b/docs/explanation/party-mode.md index 4d3483fc8..baa2e4505 100644 --- a/docs/explanation/party-mode.md +++ b/docs/explanation/party-mode.md @@ -5,7 +5,7 @@ sidebar: order: 11 --- -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. +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, how to build your own cast of personas instead of using the installed agents, and how a party remembers you between sessions. ## What is Party Mode? @@ -131,6 +131,16 @@ Whichever mode is running, the orchestrator presents the result as one conversat 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. ::: +## The room remembers + +Give a party a memory and it picks up where you left off. It keeps its own record of your past sessions — the dynamics that built up between members, the threads you left open, and where earlier conversations landed. Reopen it a week later and that history is intact: two members who came to blows last time still open a little frosty, and a sharp line from a past session can resurface as an organic callback. + +It's memory, not a transcript. The room carries the few things worth remembering, not a log of everything said, so the next conversation feels continuous without dragging the whole past into it. It happens on its own, in the background — nothing to save, and the room never breaks character to announce it. + +A character who turns up on the fly is remembered too — a walk-on from an open-cast scene, or someone you add mid-conversation. At the end of a session the room offers to keep the new arrivals, folding them into the party so they can come back next time. + +Memory is set per party. When you create or save a party you're asked whether it should remember; the default installed-agent room remembers unless you turn it off. Set or change any of this through `/bmad-customize bmad-party-mode`. + ## 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. diff --git a/src/core-skills/bmad-party-mode/SKILL.md b/src/core-skills/bmad-party-mode/SKILL.md index 3383bc356..e1cf3c59a 100644 --- a/src/core-skills/bmad-party-mode/SKILL.md +++ b/src/core-skills/bmad-party-mode/SKILL.md @@ -5,41 +5,38 @@ description: 'Orchestrates lively group discussions between installed BMAD agent # Party Mode -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 - -- **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. -- **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. -- **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. -- **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. - -If a round comes back feeling like four essays stapled together, you missed the objective. Tighten it the next round. +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. ## 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. +- **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.}` resolves to `customize.toml`'s `[workflow]` table (overrides win). +- **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. +- **File roles:** a party's memory is the per-party memlog at `{workflow.memory_dir}//.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). +- **Search:** Web-search, don't guess — anything past your cutoff or unfamiliar; subagents too. -## Setup +## On Activation -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 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. 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. -3. **Resolve the active roster:** `uv run {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. -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. - - A runtime `--party ` (alias `--group `) overrides any configured `default_party`: run `resolve_party.py --party ` for that group's full detail. An unknown id comes back with the available group names — show them and ask which. - - Run `resolve_party.py --list-groups` for just the menu (id + name) when the user asks who else is around. - - Mid-session the same levers apply: the user can switch rooms ("switch to the writers' room") — re-run `resolve_party.py --party `, 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. **Load party memory.** If `{workflow.party_memory}` is on and the active roster has a stable key — a resolved group id or the default room's `installed`, not an ad-hoc inline cast — read its memlog and let it shape how the room opens, in character. Follow `references/party-memory.md`. When the user switches rooms mid-session (step 4), load the new room's memory the same way before its faces arrive. -6. 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. +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). +2. Load `{project-root}/_bmad/core/config.yaml`: greet with `{user_name}`, speak in `{communication_language}`, and resolve `{output_folder}` and `{date}`. +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. +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 ` (alias `--group `) 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 ` and carrying the thread over, or summon any collective member by name. +5. **Memory.** If `memory_enabled` (from `resolve_party.py`), follow `references/party-memory.md` for the whole run. +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. +7. Run each `{workflow.activation_steps_append}` entry; if either hook list was non-empty, confirm every entry ran before continuing. -Then run each `{workflow.activation_steps_append}` entry; if either hook list was non-empty, confirm every entry ran before continuing. +## Keep It Feeling Like a Party -**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. +This is the bar — strive for every one of these, every round. It's the difference between a party and a panel: -**Memory accrues live, not at wrap-up.** A party usually just trails off, so when `{workflow.party_memory}` is on and the party has a stable key, record memorable beats to its memlog as they land — silently, behind the conversation — rather than waiting for an ending that may never come. Follow `references/party-memory.md`. +- **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. +- **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. +- **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. +- **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. +- **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. +- **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. +- **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. +- **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. +- **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. ## How It Runs @@ -47,38 +44,15 @@ Use `{workflow.party_mode}` for the session unless the user passed `--mode ` to summon the group, or that it's now the default if they set it. diff --git a/src/core-skills/bmad-party-mode/references/party-memory.md b/src/core-skills/bmad-party-mode/references/party-memory.md index b54ac4e32..78244d2c6 100644 --- a/src/core-skills/bmad-party-mode/references/party-memory.md +++ b/src/core-skills/bmad-party-mode/references/party-memory.md @@ -2,6 +2,8 @@ The room remembers its past sessions with this user and brings them back to life — in character. Memory is per-party and append-only. +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. + ## Where it lives 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. @@ -25,6 +27,10 @@ Writes are silent. The room never announces "noted" or "I'll remember". 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. +## New faces + +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 (" 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. + ## Write it ``` diff --git a/src/core-skills/bmad-party-mode/scripts/resolve_party.py b/src/core-skills/bmad-party-mode/scripts/resolve_party.py index bcca64af4..abee93cf3 100644 --- a/src/core-skills/bmad-party-mode/scripts/resolve_party.py +++ b/src/core-skills/bmad-party-mode/scripts/resolve_party.py @@ -197,7 +197,8 @@ def group_detail(g, collective, index): 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} + "members": members, "unresolved": unresolved, + "memory_enabled": bool(g.get("memory", False))} if g.get("scene"): detail["scene"] = g["scene"] if not raw_members: @@ -220,6 +221,9 @@ def main(): groups = workflow.get("party_groups", []) or [] default_party = workflow.get("default_party", "") or "" party_mode = workflow.get("party_mode", "session") or "session" + # The global party_memory flag governs only the DEFAULT installed-agent room; + # a named group carries its own `memory` flag (resolved in group_detail). + party_memory = bool(workflow.get("party_memory", True)) # Group menu never needs the (more expensive) installed-agent resolve. if args.list_groups: @@ -252,7 +256,8 @@ def main(): # 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]}) + "members": [collective[c] for c in installed_codes], + "memory_enabled": party_memory}) _emit(result) diff --git a/src/core-skills/bmad-party-mode/scripts/tests/test-resolve_party.py b/src/core-skills/bmad-party-mode/scripts/tests/test-resolve_party.py index 58c50a985..43aaa90c7 100644 --- a/src/core-skills/bmad-party-mode/scripts/tests/test-resolve_party.py +++ b/src/core-skills/bmad-party-mode/scripts/tests/test-resolve_party.py @@ -113,6 +113,14 @@ class TestGroupDetail(unittest.TestCase): self.assertEqual(d["members"], []) self.assertEqual(d["scene"][:7], "Figures") + def test_memory_enabled_follows_group_flag_and_defaults_off(self): + on = rp.group_detail({"id": "g", "members": ["morpheus"], "memory": True}, self.col, self.idx) + self.assertTrue(on["memory_enabled"]) + off = rp.group_detail({"id": "g", "members": ["morpheus"], "memory": False}, self.col, self.idx) + self.assertFalse(off["memory_enabled"]) + absent = rp.group_detail({"id": "g", "members": ["morpheus"]}, self.col, self.idx) + self.assertFalse(absent["memory_enabled"]) # opt-in per named group + class TestInstalledCodesIsDefaultRoom(unittest.TestCase): """The default room is installed agents only; pure customs stay in the pool."""