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/src/core-skills/bmad-party-mode/SKILL.md b/src/core-skills/bmad-party-mode/SKILL.md index bb291701b..3383bc356 100644 --- a/src/core-skills/bmad-party-mode/SKILL.md +++ b/src/core-skills/bmad-party-mode/SKILL.md @@ -24,20 +24,23 @@ If a round comes back feeling like four essays stapled together, you missed the ## Setup -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). +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:** `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. +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. 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. +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. 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. +**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`. + ## How It Runs Use `{workflow.party_mode}` for the session unless the user passed `--mode ` (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. @@ -74,4 +77,8 @@ It is your goal to keep party mode feeling like a party, a good party. fun, enga ## Wrapping Up -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. \ No newline at end of file +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, if `{workflow.party_memory}` is on and the party has a stable key, top up its memlog with the final outcome and any memorable beat not yet captured (per `references/party-memory.md`) — memory accrued live through the party, so this is a top-up, not the only write. + +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. diff --git a/src/core-skills/bmad-party-mode/customize.toml b/src/core-skills/bmad-party-mode/customize.toml index 3fcaa6675..18d936ec0 100644 --- a/src/core-skills/bmad-party-mode/customize.toml +++ b/src/core-skills/bmad-party-mode/customize.toml @@ -51,6 +51,22 @@ party_mode = "session" # config; point this elsewhere in your team/user override to redirect keepsakes. output_dir = "{output_folder}/party-mode" +# Per-party memory (the memlog standard). When on, each party keeps a succinct, +# append-only memlog that the room reads on entry and writes at wrap-up, so the +# next session opens remembering the last one — clashes and alliances between +# members carried forward, memorable moments, organic callbacks, and where past +# sessions landed. It is memory, not a transcript: a handful of entries per +# session, never a recap. Set false to disable the read and the write entirely. +# Only parties with a stable key (configured groups and the default room) have +# memory; ad-hoc inline casts are ephemeral until saved as a custom party. +party_memory = true + +# Root for the per-party memlogs. Each party stores at +# `{memory_dir}//.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 = "" 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..b54ac4e32 --- /dev/null +++ b/src/core-skills/bmad-party-mode/references/party-memory.md @@ -0,0 +1,45 @@ +# 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. + +## 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. + +## 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