Compare commits
3 Commits
aa00389027
...
d1db500e28
| Author | SHA1 | Date |
|---|---|---|
|
|
d1db500e28 | |
|
|
b0b1796227 | |
|
|
ebbb5ccb3b |
|
|
@ -47,6 +47,8 @@ CLAUDE.local.md
|
||||||
.claude/settings.local.json
|
.claude/settings.local.json
|
||||||
.junie/
|
.junie/
|
||||||
.agents/
|
.agents/
|
||||||
|
.analysis/
|
||||||
|
|
||||||
|
|
||||||
z*/
|
z*/
|
||||||
!docs/zh-cn/
|
!docs/zh-cn/
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ sidebar:
|
||||||
order: 11
|
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?
|
## 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.
|
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
|
## 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.
|
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.
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@ Activation is complete. If `activation_steps_prepend` or `activation_steps_appen
|
||||||
|
|
||||||
**Create.** A brief the user is proud of, that meets their needs, drawn out through real conversation — do not assume: instead converse and understand, and then help craft the best product brief for their needs. Begin in `## Discovery` before drafting; the brief comes after the picture is on the table. Shape follows the product and need. Treat `{workflow.brief_template}` as a starting structure, not a contract: drop sections that do not earn their place, add sections the product needs, reorder freely - create sections for specialized domains or concerns also as needed. The brief serves the product's story, not the template's shape. Bind `{doc_workspace}` to a fresh folder at `{workflow.brief_output_path}/{workflow.run_folder_pattern}/`, write `brief.md` there with YAML frontmatter (title, status, created, updated), and seed the memlog: `uv run {project-root}/_bmad/scripts/memlog.py init --workspace {doc_workspace} --field topic="<product>"`. For Update and Validate, `{doc_workspace}` is the existing folder of the brief being targeted.
|
**Create.** A brief the user is proud of, that meets their needs, drawn out through real conversation — do not assume: instead converse and understand, and then help craft the best product brief for their needs. Begin in `## Discovery` before drafting; the brief comes after the picture is on the table. Shape follows the product and need. Treat `{workflow.brief_template}` as a starting structure, not a contract: drop sections that do not earn their place, add sections the product needs, reorder freely - create sections for specialized domains or concerns also as needed. The brief serves the product's story, not the template's shape. Bind `{doc_workspace}` to a fresh folder at `{workflow.brief_output_path}/{workflow.run_folder_pattern}/`, write `brief.md` there with YAML frontmatter (title, status, created, updated), and seed the memlog: `uv run {project-root}/_bmad/scripts/memlog.py init --workspace {doc_workspace} --field topic="<product>"`. For Update and Validate, `{doc_workspace}` is the existing folder of the brief being targeted.
|
||||||
|
|
||||||
**Update.** Reconcile an existing brief with a change signal. Before proposing changes, read the brief, addendum, `.memlog.md`, and original inputs — and run the `## Discovery` posture against the change signal (a patch applied without context becomes drift). Surface conflicts with prior decisions before changing. Headless override: log the reversal via `memlog.py append --type override`, then apply; halt `blocked` if intent is ambiguous. If the change is fundamental, offer Create instead of patching.
|
**Update.** Reconcile an existing brief with a change signal. Before proposing changes, read the brief, addendum, `.memlog.md`, and original inputs — and run the `## Discovery` posture against the change signal (a patch applied without context becomes drift). If `.memlog.md` is missing (a legacy or pre-standard brief), init it with `uv run {project-root}/_bmad/scripts/memlog.py init --workspace {doc_workspace}` first — this update is its first entry. Surface conflicts with prior decisions before changing. Headless override: log the reversal via `uv run {project-root}/_bmad/scripts/memlog.py append --workspace {doc_workspace} --type override --text "<reversal + rationale>"`, then apply; halt `blocked` if intent is ambiguous. If the change is fundamental, offer Create instead of patching.
|
||||||
|
|
||||||
**Validate.** Honest critique against the brief's own purpose. Read the brief, the addendum if present, `.memlog.md`, and any original inputs first — a validation that ignores prior decisions, rejected ideas, or context the user supplied is shallow. Cite specific lines. Caveat what cannot be evaluated. Return inline — no separate file unless asked. Always offer to roll findings into an Update, even in headless mode — include `"offer_to_update": true` in the JSON status block.
|
**Validate.** Honest critique against the brief's own purpose. Read the brief, the addendum if present, `.memlog.md`, and any original inputs first — a validation that ignores prior decisions, rejected ideas, or context the user supplied is shallow. Cite specific lines. Caveat what cannot be evaluated. Return inline — no separate file unless asked. Always offer to roll findings into an Update, even in headless mode — include `"offer_to_update": true` in the JSON status block.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@ Activation is complete. If `activation_steps_prepend` or `activation_steps_appen
|
||||||
|
|
||||||
**Create.** Bind `{doc_workspace}` to `{workflow.prd_output_path}/{workflow.run_folder_pattern}/`. Write `prd.md` with YAML frontmatter (title, status, created, updated — initial `status: draft`), and seed the memlog with `uv run {project-root}/_bmad/scripts/memlog.py init --workspace {doc_workspace} --field topic="<PRD/product name>"` so subsequent decisions land in a known file. Tell the user the path. Run `## Discovery`, then `## Finalize`.
|
**Create.** Bind `{doc_workspace}` to `{workflow.prd_output_path}/{workflow.run_folder_pattern}/`. Write `prd.md` with YAML frontmatter (title, status, created, updated — initial `status: draft`), and seed the memlog with `uv run {project-root}/_bmad/scripts/memlog.py init --workspace {doc_workspace} --field topic="<PRD/product name>"` so subsequent decisions land in a known file. Tell the user the path. Run `## Discovery`, then `## Finalize`.
|
||||||
|
|
||||||
**Update.** Reconcile the PRD with a change signal. Source-extract against PRD, addendum, `.memlog.md`, and original inputs (extract, don't ingest). If `.memlog.md` is missing, `memlog.py init` it, then spawn a one-time bootstrap subagent to reverse-engineer a thin log from the PRD (one `memlog.py append` per recovered decision) before continuing. Surface conflicts with prior decisions before applying. Then `## Finalize`.
|
**Update.** Reconcile the PRD with a change signal. Source-extract against PRD, addendum, `.memlog.md`, and original inputs (extract, don't ingest). If `.memlog.md` is missing, init it with `uv run {project-root}/_bmad/scripts/memlog.py init --workspace {doc_workspace}`, then spawn a one-time bootstrap subagent to reverse-engineer a thin log from the PRD (one `uv run {project-root}/_bmad/scripts/memlog.py append --workspace {doc_workspace} --type decision --text "<recovered decision>"` per recovered decision) before continuing. Surface conflicts with prior decisions before applying. Then `## Finalize`.
|
||||||
|
|
||||||
**Validate** (or *analyze*). Critique without changing. Load `references/validate.md`.
|
**Validate** (or *analyze*). Critique without changing. Load `references/validate.md`.
|
||||||
|
|
||||||
|
|
@ -88,5 +88,5 @@ Tell the user the sequence in one sentence, then walk it. Polish goes last so it
|
||||||
4. **Triage open items.** All Open Questions, `[ASSUMPTION]` tags, `[NOTE FOR PM]` callouts. Phase-blockers (would make the PRD unsafe for UX/architecture/epics) surfaced one at a time and resolved; non-blockers deferred with owner + revisit condition logged via `memlog.py append`. If phase-blocker count is high, flag it.
|
4. **Triage open items.** All Open Questions, `[ASSUMPTION]` tags, `[NOTE FOR PM]` callouts. Phase-blockers (would make the PRD unsafe for UX/architecture/epics) surfaced one at a time and resolved; non-blockers deferred with owner + revisit condition logged via `memlog.py append`. If phase-blocker count is high, flag it.
|
||||||
5. **Polish.** Apply `{workflow.doc_standards}` to `prd.md` and `addendum.md` in declared order (structural passes before prose — prose should not polish soon-to-be-cut text). Parallelize across documents, sequential within.
|
5. **Polish.** Apply `{workflow.doc_standards}` to `prd.md` and `addendum.md` in declared order (structural passes before prose — prose should not polish soon-to-be-cut text). Parallelize across documents, sequential within.
|
||||||
6. **External handoffs.** Execute `{workflow.external_handoffs}`; surface returned URLs/IDs. Skip and flag unavailable tools.
|
6. **External handoffs.** Execute `{workflow.external_handoffs}`; surface returned URLs/IDs. Skip and flag unavailable tools.
|
||||||
7. **Close.** Set `prd.md` frontmatter `status: final` and `updated` to `{date}` so future invocations distinguish this PRD from in-progress drafts. Record finalization via `memlog.py append --type event --text "PRD finalized"`. Share artifact paths. Common next: `bmad-ux`, `bmad-architecture`, `bmad-create-epics-and-stories`; invoke `bmad-help` for authoritative routing.
|
7. **Close.** Set `prd.md` frontmatter `status: final` and `updated` to `{date}` so future invocations distinguish this PRD from in-progress drafts. Record finalization via `uv run {project-root}/_bmad/scripts/memlog.py append --workspace {doc_workspace} --type event --text "PRD finalized"`. Share artifact paths. Common next: `bmad-ux`, `bmad-architecture`, `bmad-create-epics-and-stories`; invoke `bmad-help` for authoritative routing.
|
||||||
8. Run `{workflow.on_complete}` if non-empty.
|
8. Run `{workflow.on_complete}` if non-empty.
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,7 @@ Activation is complete. If `activation_steps_prepend` or `activation_steps_appen
|
||||||
|
|
||||||
**Create.** Bind `{doc_workspace}` to `{workflow.ux_output_path}/{workflow.run_folder_pattern}/`. Create `.working/` and `imports/`; seed the memlog with `uv run {project-root}/_bmad/scripts/memlog.py init --workspace {doc_workspace} --field topic="<product/UX>"`; create `DESIGN.md` (frontmatter only) and `EXPERIENCE.md` (frontmatter only). Run Discovery → Finalize.
|
**Create.** Bind `{doc_workspace}` to `{workflow.ux_output_path}/{workflow.run_folder_pattern}/`. Create `.working/` and `imports/`; seed the memlog with `uv run {project-root}/_bmad/scripts/memlog.py init --workspace {doc_workspace} --field topic="<product/UX>"`; create `DESIGN.md` (frontmatter only) and `EXPERIENCE.md` (frontmatter only). Run Discovery → Finalize.
|
||||||
|
|
||||||
**Update.** Read spines + memlog + sources. `memlog.py init` the memlog if missing — this update is entry one. Surface conflicts with prior decisions. Run Finalize.
|
**Update.** Read spines + memlog + sources. If `.memlog.md` is missing, init it with `uv run {project-root}/_bmad/scripts/memlog.py init --workspace {doc_workspace}` — this update is entry one. Surface conflicts with prior decisions. Run Finalize.
|
||||||
|
|
||||||
**Validate.** See `references/validate.md`.
|
**Validate.** See `references/validate.md`.
|
||||||
|
|
||||||
|
|
@ -87,4 +87,4 @@ Outcomes, in order:
|
||||||
- **Key-screen mocks rendered.** Key-screens tool → `.working/` for surfaces where layout drives behavior or anchors visual language.
|
- **Key-screen mocks rendered.** Key-screens tool → `.working/` for surfaces where layout drives behavior or anchors visual language.
|
||||||
- **Mock coverage confirmed.** Walk every IA surface; classify *mocked* vs *spine-only*. Ask: *"These will be built from spine tables alone — any need a visual reference?"* Render more if named; log spine-only choices.
|
- **Mock coverage confirmed.** Walk every IA surface; classify *mocked* vs *spine-only*. Ask: *"These will be built from spine tables alone — any need a visual reference?"* Render more if named; log spine-only choices.
|
||||||
- **Layout extracted, artifacts promoted.** Distill subagent re-reads each `.working/` and `imports/` artifact; lifts visual decisions into DESIGN.md and behavioral decisions into EXPERIENCE.md. Promote `.working/` keepers to `mockups/` (HTML) or `wireframes/` (Excalidraw); imports stay. Inline relative links at relevant spine sections; state spines-win-on-conflict once.
|
- **Layout extracted, artifacts promoted.** Distill subagent re-reads each `.working/` and `imports/` artifact; lifts visual decisions into DESIGN.md and behavioral decisions into EXPERIENCE.md. Promote `.working/` keepers to `mockups/` (HTML) or `wireframes/` (Excalidraw); imports stay. Inline relative links at relevant spine sections; state spines-win-on-conflict once.
|
||||||
- **Polished, handed off, closed.** Apply `{workflow.doc_standards}` in order. Execute `{workflow.external_handoffs}`; surface URLs. Set both files' `status: final`, `updated: {date}`. Log finalization via `memlog.py append --type event --text "spines finalized"`. Share paths. Common next: `bmad-architecture`, `bmad-create-epics-and-stories`, `bmad-dev-story`. Run `{workflow.on_complete}`.
|
- **Polished, handed off, closed.** Apply `{workflow.doc_standards}` in order. Execute `{workflow.external_handoffs}`; surface URLs. Set both files' `status: final`, `updated: {date}`. Log finalization via `uv run {project-root}/_bmad/scripts/memlog.py append --workspace {doc_workspace} --type event --text "spines finalized"`. Share paths. Common next: `bmad-architecture`, `bmad-create-epics-and-stories`, `bmad-dev-story`. Run `{workflow.on_complete}`.
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ Pick by what the decision needs:
|
||||||
- **PMI (Plus / Minus / Interesting)** — when one strong candidate needs pressure-testing before commitment: list its pluses, minuses, and the merely-interesting, then judge.
|
- **PMI (Plus / Minus / Interesting)** — when one strong candidate needs pressure-testing before commitment: list its pluses, minuses, and the merely-interesting, then judge.
|
||||||
- **MoSCoW** — when scoping a build: sort into Must / Should / Could / Won't-this-time.
|
- **MoSCoW** — when scoping a build: sort into Must / Should / Could / Won't-this-time.
|
||||||
|
|
||||||
Log the surviving directions and the reasoning with `uv run {project-root}/_bmad/scripts/memlog.py append --type decision --text "<one-line gist>"` (use `--by` in Creative Partner mode). Two or three convergence moves chained is fine (e.g. cluster → score the clusters); more than that is usually over-processing.
|
Log the surviving directions and the reasoning with `uv run {project-root}/_bmad/scripts/memlog.py append --workspace {doc_workspace} --type decision --text "<one-line gist>"` (use `--by` in Creative Partner mode). Two or three convergence moves chained is fine (e.g. cluster → score the clusters); more than that is usually over-processing.
|
||||||
|
|
||||||
## Then finalize
|
## Then finalize
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ In Facilitator mode this is the one place your own creative contribution is welc
|
||||||
1. **Hand them the mirror first.** Reflect a vivid sampling of *their* ideas back — deliberately include the odd, random, or buried ones from earlier, not just the recent obvious ones (in Creative Partner mode the `(... by user)` tags tell you which were theirs). Ask what they see now: conclusions, synergies, themes, the few that actually matter. Let them connect first; their own pattern-recognition is the point.
|
1. **Hand them the mirror first.** Reflect a vivid sampling of *their* ideas back — deliberately include the odd, random, or buried ones from earlier, not just the recent obvious ones (in Creative Partner mode the `(... by user)` tags tell you which were theirs). Ask what they see now: conclusions, synergies, themes, the few that actually matter. Let them connect first; their own pattern-recognition is the point.
|
||||||
2. **Then add the connections they would miss.** Lean in creatively — not new raw ideas, but the non-obvious links: this idea from technique one quietly solves that tension from technique four; these three are one idea wearing three hats; this wildcard is the real breakthrough.
|
2. **Then add the connections they would miss.** Lean in creatively — not new raw ideas, but the non-obvious links: this idea from technique one quietly solves that tension from technique four; these three are one idea wearing three hats; this wildcard is the real breakthrough.
|
||||||
|
|
||||||
Record the insights and chosen directions with `memlog.py append --type insight`. **Then run `uv run {project-root}/_bmad/scripts/memlog.py set --workspace {doc_workspace} --key status --value complete`** — the session is done and must stop being offered for resume. Do this even if the user declines every artifact below.
|
Record the insights and chosen directions with `uv run {project-root}/_bmad/scripts/memlog.py append --workspace {doc_workspace} --type insight --text "<insights + chosen directions>"`. **Then run `uv run {project-root}/_bmad/scripts/memlog.py set --workspace {doc_workspace} --key status --value complete`** — the session is done and must stop being offered for resume. Do this even if the user declines every artifact below.
|
||||||
|
|
||||||
## Artifacts
|
## Artifacts
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -30,8 +30,8 @@ Free-form structured payload in the first message; provide what applies:
|
||||||
## Run
|
## Run
|
||||||
|
|
||||||
1. Bind `{doc_workspace}` and create the memlog with `uv run {project-root}/_bmad/scripts/memlog.py init --workspace {doc_workspace} --field topic="<topic>" [--field goal="<goal>"]`. It remains the canonical source every artifact derives from.
|
1. Bind `{doc_workspace}` and create the memlog with `uv run {project-root}/_bmad/scripts/memlog.py init --workspace {doc_workspace} --field topic="<topic>" [--field goal="<goal>"]`. It remains the canonical source every artifact derives from.
|
||||||
2. Run the divergent session per **The inversion**, capturing each idea with `memlog.py append --workspace {doc_workspace} --type idea --text "<idea>"` as it lands, and marking each technique switch with `memlog.py append --type technique --text "started <name>"`.
|
2. Run the divergent session per **The inversion**, capturing each idea with `uv run {project-root}/_bmad/scripts/memlog.py append --workspace {doc_workspace} --type idea --text "<idea>"` as it lands, and marking each technique switch with `uv run {project-root}/_bmad/scripts/memlog.py append --workspace {doc_workspace} --type technique --text "started <name>"`.
|
||||||
3. Synthesize: surface the conclusions, connections, and the few directions that matter; record them with `memlog.py append --type insight`, then run `memlog.py set --workspace {doc_workspace} --key status --value complete`.
|
3. Synthesize: surface the conclusions, connections, and the few directions that matter; record them with `uv run {project-root}/_bmad/scripts/memlog.py append --workspace {doc_workspace} --type insight --text "<insights>"`, then run `uv run {project-root}/_bmad/scripts/memlog.py set --workspace {doc_workspace} --key status --value complete`.
|
||||||
4. Produce the requested artifacts from the log — `brainstorm.html` (the imaginative, self-contained, no-template report) and/or the succinct `brainstorm-intent.md` — the same artifacts `references/finalize.md` describes, delegating each to a subagent that reads the log as its sole source. (Headless produces the `artifacts` payload directly; it does not ask, unlike the interactive opt-in.)
|
4. Produce the requested artifacts from the log — `brainstorm.html` (the imaginative, self-contained, no-template report) and/or the succinct `brainstorm-intent.md` — the same artifacts `references/finalize.md` describes, delegating each to a subagent that reads the log as its sole source. (Headless produces the `artifacts` payload directly; it does not ask, unlike the interactive opt-in.)
|
||||||
5. Execute each entry in `{workflow.external_handoffs}` (capture returned URLs/IDs into the JSON `external_handoffs` array; skip and flag unavailable tools — local files always exist). Then run `{workflow.on_complete}` if non-empty.
|
5. Execute each entry in `{workflow.external_handoffs}` (capture returned URLs/IDs into the JSON `external_handoffs` array; skip and flag unavailable tools — local files always exist). Then run `{workflow.on_complete}` if non-empty.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,265 +0,0 @@
|
||||||
# /// script
|
|
||||||
# requires-python = ">=3.10"
|
|
||||||
# dependencies = ["pytest>=8.0"]
|
|
||||||
# ///
|
|
||||||
"""Tests for memlog.py. Run: uv run --with pytest pytest scripts/tests/test_memlog.py
|
|
||||||
|
|
||||||
The spine under test is the flat, append-only, chronological invariant: every entry is
|
|
||||||
one line recorded at the end in the order it happened — no sections, no grouping.
|
|
||||||
"""
|
|
||||||
import json
|
|
||||||
import sys
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
|
|
||||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
|
||||||
import memlog # noqa: E402
|
|
||||||
|
|
||||||
MEMLOG = ".memlog.md"
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def ws(tmp_path):
|
|
||||||
return str(tmp_path)
|
|
||||||
|
|
||||||
|
|
||||||
def read(ws):
|
|
||||||
return (Path(ws) / MEMLOG).read_text(encoding="utf-8")
|
|
||||||
|
|
||||||
|
|
||||||
def body_of(ws):
|
|
||||||
return memlog.split(read(ws))[1]
|
|
||||||
|
|
||||||
|
|
||||||
def entries(ws):
|
|
||||||
return [ln for ln in body_of(ws).splitlines() if ln.startswith("- ")]
|
|
||||||
|
|
||||||
|
|
||||||
def init(ws, **fields):
|
|
||||||
fields = fields or {"topic": "Reinvent the lunchbox", "goal": "ideas for a pitch"}
|
|
||||||
argv = ["init", "--workspace", ws]
|
|
||||||
for k, v in fields.items():
|
|
||||||
argv += ["--field", f"{k}={v}"]
|
|
||||||
assert memlog.main(argv) == 0
|
|
||||||
|
|
||||||
|
|
||||||
def append(ws, text, entry_type=None, by=None):
|
|
||||||
argv = ["append", "--workspace", ws, "--text", text]
|
|
||||||
if entry_type:
|
|
||||||
argv += ["--type", entry_type]
|
|
||||||
if by:
|
|
||||||
argv += ["--by", by]
|
|
||||||
assert memlog.main(argv) == 0
|
|
||||||
|
|
||||||
|
|
||||||
# --- init ---------------------------------------------------------------
|
|
||||||
|
|
||||||
def test_init_writes_frontmatter_fields(ws):
|
|
||||||
init(ws)
|
|
||||||
meta, body = memlog.split(read(ws))
|
|
||||||
assert meta["topic"] == "Reinvent the lunchbox"
|
|
||||||
assert meta["goal"] == "ideas for a pitch"
|
|
||||||
assert meta["status"] == "active"
|
|
||||||
assert "updated" in meta
|
|
||||||
assert body.strip() == ""
|
|
||||||
|
|
||||||
|
|
||||||
def test_init_arbitrary_fields(ws):
|
|
||||||
init(ws, topic="T", audience="board")
|
|
||||||
meta, _ = memlog.split(read(ws))
|
|
||||||
assert meta["audience"] == "board"
|
|
||||||
|
|
||||||
|
|
||||||
def test_init_refuses_overwrite(ws):
|
|
||||||
init(ws)
|
|
||||||
assert memlog.main(["init", "--workspace", ws, "--field", "topic=other"]) == 2
|
|
||||||
|
|
||||||
|
|
||||||
def test_init_creates_missing_workspace(tmp_path):
|
|
||||||
nested = str(tmp_path / "a" / "b")
|
|
||||||
assert memlog.main(["init", "--workspace", nested, "--field", "topic=T"]) == 0
|
|
||||||
assert (Path(nested) / MEMLOG).is_file()
|
|
||||||
|
|
||||||
|
|
||||||
def test_init_rejects_malformed_field(ws):
|
|
||||||
assert memlog.main(["init", "--workspace", ws, "--field", "noequals"]) == 2
|
|
||||||
|
|
||||||
|
|
||||||
# --- append: flat chronological order is the whole point -----------------
|
|
||||||
|
|
||||||
def test_append_lands_at_end_in_order(ws):
|
|
||||||
init(ws)
|
|
||||||
append(ws, "first")
|
|
||||||
append(ws, "second")
|
|
||||||
append(ws, "third")
|
|
||||||
assert entries(ws) == ["- first", "- second", "- third"]
|
|
||||||
|
|
||||||
|
|
||||||
def test_no_sections_or_headings_ever(ws):
|
|
||||||
init(ws)
|
|
||||||
append(ws, "started foo", entry_type="technique")
|
|
||||||
append(ws, "an idea", entry_type="idea")
|
|
||||||
append(ws, "started bar", entry_type="technique")
|
|
||||||
assert "## " not in body_of(ws) # the flat log never grows headings
|
|
||||||
|
|
||||||
|
|
||||||
def test_type_renders_as_inline_tag(ws):
|
|
||||||
init(ws)
|
|
||||||
append(ws, "the earth revolves around the sun", entry_type="idea")
|
|
||||||
append(ws, "how do we handle stampede?", entry_type="question")
|
|
||||||
body = body_of(ws)
|
|
||||||
assert "- (idea) the earth revolves around the sun" in body
|
|
||||||
assert "- (question) how do we handle stampede?" in body
|
|
||||||
|
|
||||||
|
|
||||||
def test_append_without_type_is_plain_note(ws):
|
|
||||||
init(ws)
|
|
||||||
append(ws, "bare entry")
|
|
||||||
assert entries(ws) == ["- bare entry"]
|
|
||||||
|
|
||||||
|
|
||||||
def test_append_collapses_newlines_into_one_line(ws):
|
|
||||||
init(ws)
|
|
||||||
append(ws, "line one\nline two\n spaced out")
|
|
||||||
assert entries(ws) == ["- line one line two spaced out"]
|
|
||||||
|
|
||||||
|
|
||||||
def test_revisited_technique_is_just_a_later_entry(ws):
|
|
||||||
# the user's model: switching techniques is an entry, not a section to return to
|
|
||||||
init(ws)
|
|
||||||
append(ws, "started SCAMPER", entry_type="technique")
|
|
||||||
append(ws, "magnetic latch", entry_type="idea")
|
|
||||||
append(ws, "started Six Hats", entry_type="technique")
|
|
||||||
append(ws, "stale data risk", entry_type="idea")
|
|
||||||
append(ws, "started SCAMPER", entry_type="technique") # back to SCAMPER — just appended again
|
|
||||||
append(ws, "stackable tiers", entry_type="idea")
|
|
||||||
assert entries(ws) == [
|
|
||||||
"- (technique) started SCAMPER",
|
|
||||||
"- (idea) magnetic latch",
|
|
||||||
"- (technique) started Six Hats",
|
|
||||||
"- (idea) stale data risk",
|
|
||||||
"- (technique) started SCAMPER",
|
|
||||||
"- (idea) stackable tiers",
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
def test_by_renders_attribution_in_tag(ws):
|
|
||||||
# Creative Partner mode must record whose idea each one was
|
|
||||||
init(ws)
|
|
||||||
append(ws, "magnetic latch lid", entry_type="idea", by="user")
|
|
||||||
append(ws, "lid doubles as a plate", entry_type="idea", by="coach")
|
|
||||||
body = body_of(ws)
|
|
||||||
assert "- (idea by user) magnetic latch lid" in body
|
|
||||||
assert "- (idea by coach) lid doubles as a plate" in body
|
|
||||||
|
|
||||||
|
|
||||||
def test_by_without_type_renders_alone(ws):
|
|
||||||
init(ws)
|
|
||||||
append(ws, "off-the-cuff thought", by="coach")
|
|
||||||
assert entries(ws) == ["- (by coach) off-the-cuff thought"]
|
|
||||||
|
|
||||||
|
|
||||||
def test_heterogeneous_entry_types_coexist(ws):
|
|
||||||
init(ws)
|
|
||||||
append(ws, "an idea", entry_type="idea")
|
|
||||||
append(ws, "an open question", entry_type="question")
|
|
||||||
append(ws, "a decision we made", entry_type="decision")
|
|
||||||
append(ws, "user wants mobile-first", entry_type="direction")
|
|
||||||
body = body_of(ws)
|
|
||||||
for tag in ("(idea)", "(question)", "(decision)", "(direction)"):
|
|
||||||
assert tag in body
|
|
||||||
|
|
||||||
|
|
||||||
# --- set ----------------------------------------------------------------
|
|
||||||
|
|
||||||
def test_set_flips_status(ws):
|
|
||||||
init(ws)
|
|
||||||
memlog.main(["set", "--workspace", ws, "--key", "status", "--value", "complete"])
|
|
||||||
assert memlog.split(read(ws))[0]["status"] == "complete"
|
|
||||||
|
|
||||||
|
|
||||||
def test_set_preserves_body(ws):
|
|
||||||
init(ws)
|
|
||||||
append(ws, "keep me", entry_type="idea")
|
|
||||||
memlog.main(["set", "--workspace", ws, "--key", "status", "--value", "complete"])
|
|
||||||
meta, body = memlog.split(read(ws))
|
|
||||||
assert meta["status"] == "complete"
|
|
||||||
assert "- (idea) keep me" in body
|
|
||||||
|
|
||||||
|
|
||||||
def test_set_can_add_new_field(ws):
|
|
||||||
init(ws)
|
|
||||||
memlog.main(["set", "--workspace", ws, "--key", "owner", "--value", "BMad"])
|
|
||||||
assert memlog.split(read(ws))[0]["owner"] == "BMad"
|
|
||||||
|
|
||||||
|
|
||||||
def test_updated_stays_last(ws):
|
|
||||||
init(ws)
|
|
||||||
memlog.main(["set", "--workspace", ws, "--key", "owner", "--value", "BMad"])
|
|
||||||
meta = memlog.split(read(ws))[0]
|
|
||||||
assert list(meta)[-1] == "updated"
|
|
||||||
|
|
||||||
|
|
||||||
# --- robustness ---------------------------------------------------------
|
|
||||||
|
|
||||||
def test_roundtrip_render_is_stable(ws):
|
|
||||||
init(ws)
|
|
||||||
append(ws, "one", entry_type="idea")
|
|
||||||
first = read(ws)
|
|
||||||
meta, body = memlog.split(first)
|
|
||||||
assert memlog.render(meta, body) == first
|
|
||||||
|
|
||||||
|
|
||||||
def test_commas_in_field_survive(ws):
|
|
||||||
init(ws, topic="cars, trains, and planes")
|
|
||||||
append(ws, "z", entry_type="idea")
|
|
||||||
meta, _ = memlog.split(read(ws))
|
|
||||||
assert meta["topic"] == "cars, trains, and planes"
|
|
||||||
|
|
||||||
|
|
||||||
def test_triple_dash_in_field_does_not_corrupt_frontmatter(ws):
|
|
||||||
# A `---` inside a value must NOT be read as the closing fence: topic stays intact,
|
|
||||||
# status survives, and the body never leaks frontmatter text.
|
|
||||||
init(ws, topic="Pricing --- tiers --- and add-ons")
|
|
||||||
append(ws, "an idea", entry_type="idea")
|
|
||||||
meta, body = memlog.split(read(ws))
|
|
||||||
assert meta["topic"] == "Pricing --- tiers --- and add-ons"
|
|
||||||
assert meta["status"] == "active"
|
|
||||||
assert entries(ws) == ["- (idea) an idea"]
|
|
||||||
assert "status:" not in body # frontmatter never bled into the body
|
|
||||||
|
|
||||||
|
|
||||||
def test_triple_dash_status_survives_in_ack(ws, capsys):
|
|
||||||
init(ws, topic="a --- b")
|
|
||||||
append(ws, "x", entry_type="idea")
|
|
||||||
out = json.loads(capsys.readouterr().out.strip().splitlines()[-1])
|
|
||||||
assert out["status"] == "active" # not "" — frontmatter recovered cleanly
|
|
||||||
|
|
||||||
|
|
||||||
def test_newline_in_field_is_neutralized(ws):
|
|
||||||
# A value carrying a newline can't break the fence on the next round-trip.
|
|
||||||
memlog.main(["init", "--workspace", ws, "--field", "topic=line one\nline two"])
|
|
||||||
append(ws, "x", entry_type="idea")
|
|
||||||
meta, _ = memlog.split(read(ws))
|
|
||||||
assert "\n" not in meta["topic"]
|
|
||||||
assert meta["status"] == "active"
|
|
||||||
|
|
||||||
|
|
||||||
def test_append_emits_json_ack(ws, capsys):
|
|
||||||
init(ws)
|
|
||||||
append(ws, "x", entry_type="idea")
|
|
||||||
out = json.loads(capsys.readouterr().out.strip().splitlines()[-1])
|
|
||||||
assert out["ok"] is True
|
|
||||||
assert out["status"] == "active"
|
|
||||||
assert out["entries"] == 1
|
|
||||||
assert out["memlog"].endswith(MEMLOG)
|
|
||||||
assert "section" not in out # sections are gone
|
|
||||||
|
|
||||||
|
|
||||||
def test_ack_entry_count_climbs(ws, capsys):
|
|
||||||
init(ws)
|
|
||||||
append(ws, "a")
|
|
||||||
append(ws, "b")
|
|
||||||
out = json.loads(capsys.readouterr().out.strip().splitlines()[-1])
|
|
||||||
assert out["entries"] == 2
|
|
||||||
|
|
@ -5,38 +5,38 @@ description: 'Orchestrates lively group discussions between installed BMAD agent
|
||||||
|
|
||||||
# Party Mode
|
# 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.
|
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.
|
||||||
|
|
||||||
**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.
|
|
||||||
|
|
||||||
## Conventions
|
## 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.<name>}` 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}/<party>/.memlog.md`; custom members and groups live in the user's `customize.toml` overrides. Mechanics in `references/party-memory.md` (memory) and `references/create-party.md` (authoring).
|
||||||
|
- **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).
|
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}` for the wrap-up keepsake.
|
2. Load `{project-root}/_bmad/core/config.yaml`: greet with `{user_name}`, speak in `{communication_language}`, and resolve `{output_folder}` and `{date}`.
|
||||||
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. **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. **Roster overrides:**
|
4. **Resolve the roster:** `uv run {skill-root}/scripts/resolve_party.py --project-root {project-root} --skill {skill-root}`. It returns the active roster (`{workflow.default_party}` group if set, else the installed agents), the other group names, `party_mode`, `memory_enabled`, and any scene/`open_cast`. Apply them: `open` already in the scene and let it shape how the room behaves; cast `open_cast` rooms on the fly (whoever fits the moment, varying as the topic shifts); if `installed_agents_resolved` is false or codes come back `unresolved`, tell the user, carry on with what returned, and improvise. Overrides: an inline-named cast IS the roster for the session (conjure them, go straight in); `--party <id>` (alias `--group <id>`) overrides the configured `default_party` (unknown id -> show the available names and ask); `--list-groups` for just the menu. Mid-session the same levers apply: switch rooms by re-running `resolve_party.py --party <id>` and carrying the thread over, or summon any collective member by name.
|
||||||
- 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.
|
5. **Memory.** If `memory_enabled` (from `resolve_party.py`), follow `references/party-memory.md` for the whole run.
|
||||||
- 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.
|
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.
|
||||||
- Run `resolve_party.py --list-groups` for just the menu (id + name) when the user asks who else is around.
|
7. Run each `{workflow.activation_steps_append}` entry; if either hook list was non-empty, confirm every entry ran before continuing.
|
||||||
- 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.
|
## 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
|
## How It Runs
|
||||||
|
|
||||||
|
|
@ -44,34 +44,15 @@ Use `{workflow.party_mode}` for the session unless the user passed `--mode <sess
|
||||||
|
|
||||||
- **`session`** — voice every persona inline, one mind behind every voice. The floor every other mode degrades to; needs no extra instructions.
|
- **`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`.
|
- **`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`.
|
- **`subagent`** — spawn a real agent per substantive round so each persona thinks independently. Load `references/mode-subagent.md`, favor faster cheaper models if available for each subagent.
|
||||||
- **`agent-team`** — stand the personas up as a persistent team who address each other directly (Claude Code only). Load `references/mode-agent-team.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`.
|
||||||
|
|
||||||
**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.
|
|
||||||
|
|
||||||
## Make It Feel Like One Conversation
|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
## Always Holds
|
|
||||||
|
|
||||||
- **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.
|
|
||||||
|
|
||||||
## Following the User's Lead
|
|
||||||
|
|
||||||
The user steers — whatever they raise, serve the conversation: any combination, any time, from one voice to the whole table.
|
|
||||||
|
|
||||||
## Keeping It Healthy
|
|
||||||
|
|
||||||
- **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?
|
|
||||||
- **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, 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.
|
When the user signals done (read the room — don't wait for a magic word):
|
||||||
|
|
||||||
|
- Read back the best takeaways.
|
||||||
|
- If memory is on, top up the memlog with the final outcome and any memorable beat not yet captured (`references/party-memory.md`) — a top-up; memory accrued live.
|
||||||
|
- Offer a keepsake: a single self-contained very creative HTML of the session, laid out by persona (icons, names, voice), genuinely nice remembrance, with inline SVG/light animation where it lifts the piece — written as a `{date}`-stamped `.html` into `{workflow.output_dir}/`, or wherever they ask.
|
||||||
|
- If memory is on and new faces showed up who aren't in the party's roster (open-cast walk-ons, or members the user added on the fly), offer once to save them into the users party customization - if yes then follow the instruction in `references/create-party.md` (declinable; don't stall the close).
|
||||||
|
- Run `{workflow.on_complete}` if non-empty, then drop back to normal mode.
|
||||||
|
|
|
||||||
|
|
@ -51,6 +51,22 @@ party_mode = "session"
|
||||||
# config; point this elsewhere in your team/user override to redirect keepsakes.
|
# config; point this elsewhere in your team/user override to redirect keepsakes.
|
||||||
output_dir = "{output_folder}/party-mode"
|
output_dir = "{output_folder}/party-mode"
|
||||||
|
|
||||||
|
# Memory for the DEFAULT room (the installed-agent party). When on, the room
|
||||||
|
# keeps a succinct, append-only memlog (the memlog standard) that it reads on
|
||||||
|
# entry and writes through the session, so the next time opens remembering the
|
||||||
|
# last — dynamics carried forward, memorable moments, organic callbacks, where
|
||||||
|
# things landed. It is memory, not a transcript. Set false to turn the default
|
||||||
|
# room's memory off. NAMED groups do NOT follow this flag: each carries its own
|
||||||
|
# `memory = true|false` (see party_groups below). Ad-hoc inline casts are always
|
||||||
|
# ephemeral until saved as a party.
|
||||||
|
party_memory = true
|
||||||
|
|
||||||
|
# Root for the per-party memlogs. Each party stores at
|
||||||
|
# `{memory_dir}/<party>/.memlog.md`, where `<party>` 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
|
# Executed when the party wraps (after the read-back, before dropping to normal
|
||||||
# mode). String scalar = one instruction; array = instructions run in order.
|
# mode). String scalar = one instruction; array = instructions run in order.
|
||||||
on_complete = ""
|
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
|
# 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.
|
# 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:
|
# More examples to drop into your override TOML:
|
||||||
# [[workflow.party_groups]] # anchored room with a scene
|
# [[workflow.party_groups]] # anchored room with a scene
|
||||||
# id = "writers-room"
|
# id = "writers-room"
|
||||||
# name = "The Writers' Room"
|
# name = "The Writers' Room"
|
||||||
# scene = "Late-night room, everyone a little punchy. Pitch hard, kill darlings faster."
|
# scene = "Late-night room, everyone a little punchy. Pitch hard, kill darlings faster."
|
||||||
# members = ["analyst", "tech-writer", "morpheus"]
|
# members = ["analyst", "tech-writer", "morpheus"]
|
||||||
|
# memory = true
|
||||||
#
|
#
|
||||||
# [[workflow.party_groups]] # open-cast room (no roster; the scene casts it)
|
# [[workflow.party_groups]] # open-cast room (no roster; the scene casts it)
|
||||||
# id = "star-wars-rebels"
|
# id = "star-wars-rebels"
|
||||||
# name = "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."
|
# 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]]
|
[[workflow.party_groups]]
|
||||||
|
|
@ -148,3 +172,4 @@ id = "code-review-crew"
|
||||||
name = "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."
|
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"]
|
members = ["sec-hawk", "adversary", "edge-hunter", "craftsman", "shipper"]
|
||||||
|
memory = false # each review stands on its own; flip to true to remember past reviews
|
||||||
|
|
|
||||||
|
|
@ -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`:
|
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_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.
|
- `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.
|
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.
|
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
|
## 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)
|
## 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
|
## 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 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.
|
- 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
|
## 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 <id>` to summon the group, or that it's now the default if they set it.
|
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,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 ("<name> turned up and …") so a recurring face can return next session. At wrap-up these are the faces the room offers to keep, saved into the party's roster through `references/create-party.md` (which writes via `bmad-customize`). Until saved they live only in the memlog, and the room re-conjures them from there.
|
||||||
|
|
||||||
|
## Write it
|
||||||
|
|
||||||
|
```
|
||||||
|
uv run {project-root}/_bmad/scripts/memlog.py append \
|
||||||
|
--workspace {workflow.memory_dir}/{active} \
|
||||||
|
--type <dynamic|moment|callback|outcome> \
|
||||||
|
--text "<one succinct line, in the room's own read of it>"
|
||||||
|
```
|
||||||
|
|
||||||
|
Add `--by <persona-code>` when a memory belongs to one character. Choose `init` vs `append` from the existence fact you already hold: the entry-read (and, on a mid-session room switch, that room's read) told you whether the memlog exists — `init --workspace {workflow.memory_dir}/{active}` once before the first append when it doesn't, plain `append` when it does. (`init` errors if the file already exists, so don't call it blind.)
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
@ -197,7 +197,8 @@ def group_detail(g, collective, index):
|
||||||
raw_members = g.get("members", []) or []
|
raw_members = g.get("members", []) or []
|
||||||
members, unresolved = resolve_members(raw_members, collective, index)
|
members, unresolved = resolve_members(raw_members, collective, index)
|
||||||
detail = {"active": g["id"], "name": g.get("name", g["id"]),
|
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"):
|
if g.get("scene"):
|
||||||
detail["scene"] = g["scene"]
|
detail["scene"] = g["scene"]
|
||||||
if not raw_members:
|
if not raw_members:
|
||||||
|
|
@ -220,6 +221,9 @@ def main():
|
||||||
groups = workflow.get("party_groups", []) or []
|
groups = workflow.get("party_groups", []) or []
|
||||||
default_party = workflow.get("default_party", "") or ""
|
default_party = workflow.get("default_party", "") or ""
|
||||||
party_mode = workflow.get("party_mode", "session") or "session"
|
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.
|
# Group menu never needs the (more expensive) installed-agent resolve.
|
||||||
if args.list_groups:
|
if args.list_groups:
|
||||||
|
|
@ -252,7 +256,8 @@ def main():
|
||||||
# No default group: the installed agents (custom additions stay in the
|
# No default group: the installed agents (custom additions stay in the
|
||||||
# pool but don't crowd the default room), exactly like a plain install.
|
# pool but don't crowd the default room), exactly like a plain install.
|
||||||
result.update({"active": "installed",
|
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)
|
_emit(result)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -113,6 +113,14 @@ class TestGroupDetail(unittest.TestCase):
|
||||||
self.assertEqual(d["members"], [])
|
self.assertEqual(d["members"], [])
|
||||||
self.assertEqual(d["scene"][:7], "Figures")
|
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):
|
class TestInstalledCodesIsDefaultRoom(unittest.TestCase):
|
||||||
"""The default room is installed agents only; pure customs stay in the pool."""
|
"""The default room is installed agents only; pure customs stay in the pool."""
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue