From b0b1796227a042af1580460dc6a75f00a0707482 Mon Sep 17 00:00:00 2001 From: Brian Date: Sat, 20 Jun 2026 17:44:21 -0500 Subject: [PATCH] feat(party-mode): persistent per-party memory (#2484) * feat(party-mode): add persistent per-party memory Each party now keeps a succinct, append-only memlog (the memlog standard) under {memory_dir}//, so a room remembers prior sessions and opens in character carrying them forward. - Memory accrues live: capture memorable beats as they land, with a floor so an abandoned session still leaves a trace; wrap-up is a top-up. - Read distills via a reader subagent that returns only current standing state (latest dynamic per pair, open threads, recent callbacks) so the raw log never enters the party context. - Writes are silent and fail safe: a missing or erroring memlog.py is skipped without breaking the fiction. - New customize knobs: party_memory (on by default) and memory_dir. Keyed per party (group id, or `installed` for the default room); ad-hoc casts stay ephemeral. On-disk compaction is left to a future memlog.py pass. * refactor(party-mode): standard structure, per-group memory, keep on-the-fly cast - Restructure SKILL.md to the standard skill shape (intro -> Conventions -> On Activation -> content); consolidate all performance rules into one "Keep It Feeling Like a Party" section. SKILL.md ~500 tokens lighter. - Per-group `memory` flag: global party_memory now governs only the default room; resolve_party.py resolves memory_enabled per active roster (default room -> party_memory, named group -> own flag), with tests. - On-the-fly characters are captured as memlog entries during a session; at wrap-up the room offers to save them into the party via bmad-customize. - Memory mechanics consolidated into references/party-memory.md; SKILL.md step 5 just routes to it. - Docs updated. * docs(party-mode): fix open-cast lock-down claim and python3->uv run in create-party --- .gitignore | 2 + docs/explanation/party-mode.md | 12 ++- src/core-skills/bmad-party-mode/SKILL.md | 85 +++++++------------ .../bmad-party-mode/customize.toml | 25 ++++++ .../references/create-party.md | 15 ++-- .../references/party-memory.md | 51 +++++++++++ .../bmad-party-mode/scripts/resolve_party.py | 9 +- .../scripts/tests/test-resolve_party.py | 8 ++ 8 files changed, 147 insertions(+), 60 deletions(-) create mode 100644 src/core-skills/bmad-party-mode/references/party-memory.md diff --git a/.gitignore b/.gitignore index 99e48d9ab..b903b294a 100644 --- a/.gitignore +++ b/.gitignore @@ -47,6 +47,8 @@ CLAUDE.local.md .claude/settings.local.json .junie/ .agents/ +.analysis/ + z*/ !docs/zh-cn/ 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 bb291701b..e1cf3c59a 100644 --- a/src/core-skills/bmad-party-mode/SKILL.md +++ b/src/core-skills/bmad-party-mode/SKILL.md @@ -5,38 +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:** `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. 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:** `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. -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. 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: + +- **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 @@ -44,34 +44,15 @@ Use `{workflow.party_mode}` for the session unless the user passed `--mode /.memlog.md`, where `` is the group id (or +# `installed` for the default room). `{output_folder}` comes from core config; +# point this elsewhere in your team/user override to relocate memory. +memory_dir = "{output_folder}/party-mode/memories" + # 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 = "" @@ -130,17 +146,25 @@ persona = "Counters the perfectionists so the room isn't a pile-on. 'Does this a # 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. # +# `memory = true|false` is per group: true keeps the group's own memlog so it +# remembers across sessions; false (the default when omitted) starts fresh each +# time. The create/save/update-party flow asks when you don't say. Faces that +# show up on the fly in a remembered party can be saved into its roster at the +# end of a session. +# # 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"] +# memory = true # # [[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." +# memory = true # --------------------------------------------------------------------------- [[workflow.party_groups]] @@ -148,3 +172,4 @@ 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"] +memory = false # each review stands on its own; flip to true to remember past reviews diff --git a/src/core-skills/bmad-party-mode/references/create-party.md b/src/core-skills/bmad-party-mode/references/create-party.md index feeaa1deb..a0f33340e 100644 --- a/src/core-skills/bmad-party-mode/references/create-party.md +++ b/src/core-skills/bmad-party-mode/references/create-party.md @@ -7,7 +7,7 @@ A guided authoring flow that turns an idea — a themed cast, a one-off persona, 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. +- `[[workflow.party_groups]]` — when the personas form a named room: `id`, `name`, an optional freeform `scene`, `members` (codes), and `memory` (`true`/`false`). `members` is optional: leave it off for an open-cast room whose `scene` names a pool the model casts from on the fly. `memory` is whether the group remembers across sessions; ask the user when they don't say, default `false`. - `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. @@ -26,11 +26,15 @@ Open by understanding what they're building. Three common shapes — stay open, 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. +**Persisting a cast already in play.** When you arrive here from a live session — the user spun up an ad-hoc cast inline and wants to keep it — the personas are already drafted and voiced. Don't re-interrogate: capture them as they've been playing, give the group an `id` and name, ask the memory and default questions, and go straight to the write. ## 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. +When the user wants to change a party that already exists (retune a member's persona, add someone to a group, swap the default), read the current state first so you change rather than clobber: `uv run {project-root}/_bmad/scripts/resolve_customization.py --skill {skill-root} --key workflow` returns the merged `party_members`, `party_groups`, and `default_party`. Show the member or group being touched, capture only the delta with the user, and hand that sparse change to `bmad-customize` — it replaces a `party_members`/`party_groups` entry whose `code`/`id` matches and appends the rest, so an edit is just the changed entry, never a full rewrite. + +## Keeping new faces from a session + +At the end of a remembered party, the room offers to keep the faces that showed up but aren't in its roster — characters cast from an open-cast scene, or members the user added on the fly. They're already drafted and voiced, so don't re-interrogate: capture each as they played (`code`, `name`, `icon`, a one-line `title`, and a `persona` drawn from how they came across), then add them as `party_members`. For a fixed-roster group, also list their codes in the group's `members` so they return as regulars. For an open-cast room, leave `members` empty — listing any member turns the room into a fixed roster and kills its on-the-fly casting; the saved personas now live in the collective, so the scene still names them and they can return without locking the room down. Hand that sparse delta to `bmad-customize` — for a built-in party with no override yet it creates one; for an existing override it merges the new members in. ## Distill from source data (when provided) @@ -54,12 +58,13 @@ Keep pushing for specificity. "Skeptical CFO" is a placeholder; "won't approve a ## 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 party should remember across sessions** (unless the user already said). Yes → `memory = true` on the group; no → `memory = false`. One-offs with no group skip this — memory is a group setting. - 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. +**First, check for code collisions.** A custom member whose `code` matches an installed agent silently *overrides* that agent in the collective. Before composing, resolve the collective once — `uv run {skill-root}/scripts/resolve_party.py --project-root {project-root} --skill {skill-root}` — and check each new member's `code` against the returned members. On a collision, surface it ("`analyst` would override the installed Analyst — intended, or pick a different code?") and let the user confirm or rename. One check, not a gate. -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. +Compose the sparse override and hand it to `bmad-customize` to place, confirm, and write — target skill `bmad-party-mode`, `[workflow]` surface. Default to the **user** override (`bmad-party-mode.user.toml`); offer the **team** file when the party is meant to be shared. Hand it the exact entries: the `party_members` tables, any `party_groups` table (including its `memory` flag), and `default_party` if the user opted in. Keep it sparse — only the new entries, never a copy of the base customize.toml. `bmad-customize` shows the TOML, waits for an explicit yes, writes, and verifies the merge; don't write the file yourself. After it lands, tell the user how to use it: `--party ` 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 new file mode 100644 index 000000000..78244d2c6 --- /dev/null +++ b/src/core-skills/bmad-party-mode/references/party-memory.md @@ -0,0 +1,51 @@ +# Party Memory + +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. + +## Read it on entry — distill, don't dump + +The log is append-only and grows every session, so don't pull the raw file into the party. Hand a reader subagent the memlog path (`{workflow.memory_dir}/{active}/.memlog.md`) and have it return a compact brief — a few hundred tokens of *where things stand now*, ready to play in character. + +Then let the brief shape the room from the first beat, **in character**: behavioral state resumes (a cold pair opens cold, an alliance opens warm), threads pick up, callbacks land when they fit — organically, not recited on sight. Never break the fourth wall: the room *remembers*; it never announces it loaded anything, and forces nothing that doesn't fit. + +## When to write + +- **When a memorable beat lands** — a clash that shifts the room's temperature, an alliance forming, a line worth a future callback, a decision, an outcome. +- **A floor.** Once a couple of real exchanges are in from the start, even if nothing dramatic happened, capture what it's about and the opening dynamic. + +At wrap-up, if the user does signal done, top up with the final outcome and anything memorable not yet captured. + +Writes are silent. The room never announces "noted" or "I'll remember". + +## What's worth remembering + +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 + +``` +uv run {project-root}/_bmad/scripts/memlog.py append \ + --workspace {workflow.memory_dir}/{active} \ + --type \ + --text "" +``` + +Add `--by ` when a memory belongs to one character. Choose `init` vs `append` from the existence fact you already hold: the entry-read (and, on a mid-session room switch, that room's read) told you whether the memlog exists — `init --workspace {workflow.memory_dir}/{active}` once before the first append when it doesn't, plain `append` when it does. (`init` errors if the file already exists, so don't call it blind.) + +If `memlog.py` is unavailable or a write errors, skip it silently and never stall the party on a failed write. + +## Forget + +The memlog is append-only by design — no surgical delete. To wipe a party's memory, delete its folder (`{workflow.memory_dir}/{active}/`). To correct a wrong memory, append a new entry that supersedes it; the room reads the latest state. + +Keep entries sparse. The distilled read keeps the *room* lean no matter how big the log gets, but the on-disk file still grows append-only. \ No newline at end of file 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."""