From 2766e7c45534369f5b1592644a7152ead0acdac0 Mon Sep 17 00:00:00 2001 From: Brian Madison Date: Sat, 30 May 2026 10:35:39 -0500 Subject: [PATCH 1/8] Initial draft: brainstorming improved --- .../bmad-brainstorming/.decision-log.md | 230 ++++++++++ src/core-skills/bmad-brainstorming/SKILL.md | 88 +++- .../assets/brain-methods.csv | 101 +++++ .../assets/techniques/six-thinking-hats.md | 14 + .../bmad-brainstorming/brain-methods.csv | 62 --- .../bmad-brainstorming/customize.toml | 80 ++++ .../bmad-brainstorming/references/headless.md | 54 +++ .../bmad-brainstorming/scripts/brain.py | 150 +++++++ .../scripts/tests/test_brain.py | 113 +++++ .../steps/step-01-session-setup.md | 214 ---------- .../steps/step-01b-continue.md | 124 ------ .../steps/step-02a-user-selected.md | 229 ---------- .../steps/step-02b-ai-recommended.md | 239 ----------- .../steps/step-02c-random-selection.md | 211 --------- .../steps/step-02d-progressive-flow.md | 266 ------------ .../steps/step-03-technique-execution.md | 403 ------------------ .../steps/step-04-idea-organization.md | 305 ------------- .../bmad-brainstorming/template.md | 15 - .../bmad-brainstorming/workflow.md | 53 --- 19 files changed, 828 insertions(+), 2123 deletions(-) create mode 100644 src/core-skills/bmad-brainstorming/.decision-log.md create mode 100644 src/core-skills/bmad-brainstorming/assets/brain-methods.csv create mode 100644 src/core-skills/bmad-brainstorming/assets/techniques/six-thinking-hats.md delete mode 100644 src/core-skills/bmad-brainstorming/brain-methods.csv create mode 100644 src/core-skills/bmad-brainstorming/customize.toml create mode 100644 src/core-skills/bmad-brainstorming/references/headless.md create mode 100644 src/core-skills/bmad-brainstorming/scripts/brain.py create mode 100644 src/core-skills/bmad-brainstorming/scripts/tests/test_brain.py delete mode 100644 src/core-skills/bmad-brainstorming/steps/step-01-session-setup.md delete mode 100644 src/core-skills/bmad-brainstorming/steps/step-01b-continue.md delete mode 100644 src/core-skills/bmad-brainstorming/steps/step-02a-user-selected.md delete mode 100644 src/core-skills/bmad-brainstorming/steps/step-02b-ai-recommended.md delete mode 100644 src/core-skills/bmad-brainstorming/steps/step-02c-random-selection.md delete mode 100644 src/core-skills/bmad-brainstorming/steps/step-02d-progressive-flow.md delete mode 100644 src/core-skills/bmad-brainstorming/steps/step-03-technique-execution.md delete mode 100644 src/core-skills/bmad-brainstorming/steps/step-04-idea-organization.md delete mode 100644 src/core-skills/bmad-brainstorming/template.md delete mode 100644 src/core-skills/bmad-brainstorming/workflow.md diff --git a/src/core-skills/bmad-brainstorming/.decision-log.md b/src/core-skills/bmad-brainstorming/.decision-log.md new file mode 100644 index 000000000..4fc0a78d9 --- /dev/null +++ b/src/core-skills/bmad-brainstorming/.decision-log.md @@ -0,0 +1,230 @@ +# Decision Log — bmad-brainstorming rebuild + +Canonical memory for the rebuild of `bmad-brainstorming` from the legacy numbered-micro-file +architecture into the outcome-driven pattern of bmad-product-brief / bmad-prd / bmad-ux. + +## Session 2026-05-30 + +**Intent:** Edit / rebuild existing skill (in place at `src/core-skills/bmad-brainstorming`). + +**Classification:** Simple Workflow with one quarantine carve-out. Customizable = yes. + +### Load-bearing decisions + +- **Single intent: pure facilitation + continuation.** No create/update/validate split (that pattern fits + document skills; brainstorming is a facilitated session). The skill facilitates a live session and can + resume a prior one. Decided with user. +- **The running log is the memory AND the continuation state.** One terse, append-only session file holds the + goal, the techniques selected, and every idea the user generates/accepts. Kept deliberately lean to minimize + context cost; the final artifacts are produced from it. Borrowed shape from Obra Superpowers' append-only + event log (one entry per line). User: "very succinct as memory, just enough to minimize context." +- **HARD RULE — the LLM never supplies its own ideas in interactive mode.** Its job is to draw ideas out of the + user and push past the obvious. If the user explicitly asks for an idea, give ONE as a one-off, then + immediately return to pushing the user. This inverts a default LLM behavior, so it earns forceful statement + with rationale. User-stated, emphasized. +- **Headless is the ONLY context where the LLM brainstorms on its own.** Given just a topic, headless runs the + techniques itself and produces the artifacts. Because it inverts the central no-own-ideas rule, ALL headless + instructions live in `references/headless.md` and are never loaded in normal mode — quarantined so they can't + bleed into and corrupt the interactive posture. User's explicit requirement. +- **Technique selection: AI-led, others on request.** Facilitator reads the goal and proposes a fitting + technique by default; "browse the library", "surprise me" (random), and "start broad then narrow" + (progressive) are always available but never a forced numbered-menu gate. Builder recommended; user accepted + ("show what you recommend and why"). +- **Finalize → imaginative HTML, no template.** Instruction directs the LLM to be genuinely creative and + whimsical, matched to the spirit of *this* session — sections per technique, creative visualizations, the + summary rendered as it all came together. Explicitly NO template/scaffold (rejected Obra's CSS-class kit). + User: pure per-session creativity. +- **Optional second artifact: succinct brainstorm-intent doc.** User's choice at finalize. Short, sharp, just + the chosen/critical discoveries — clean input for a downstream skill (product-brief, prd) without report + bloat. This is the dual-output distillate pattern. + +- **Synthesis is two-move and is the one interactive moment the LLM adds its own thinking.** First reflect a + sampling of the user's own ideas back (including surprising/random ones from across the session) and let THEM + hunt for conclusions/synergies/themes/what-matters. THEN the LLM leans in creatively to surface the + non-obvious connections the user wouldn't spot right away. The no-own-ideas rule is therefore scoped + explicitly to the *generative* phase so synthesis doesn't collide with it. User refinement. +- **The log is the load-bearing artifact; artifacts are derivations.** "From the decision log we can create any + artifact the user wants for any purpose, with nothing lost." HTML + intent doc are two derivations; others + available on request. (User flagged a future cross-cutting task: retune other skills' `.decision-log.md` to + this same lean append-only standard — NOT part of this build.) + +- **Extensible technique library via customize.toml** (new capability the static-CSV architecture never had). + Two scalars: `favorite_techniques` (names the facilitator proposes first when they fit, AI-led default) and + `additional_techniques` (array-of-tables mirroring the CSV shape `category`/`technique_name`/`description` — + adds whole new techniques AND categories without editing the shipped CSV). Both append-merge so team + user + files each contribute. Wired into SKILL.md `## Choosing Techniques` (proposing/browse/random/progressive all + treat additions as first-class) and into headless.md selection. User idea, added post-build. + +### Borrowed from Obra Superpowers brainstorming skill (researched) + +- **One-question/prompt-per-message** as a hard rule — keeps the human in active generation, operationalizes + "push their creative muscles." +- **Append-only lean log** as continuation memory. +- Rejected: AI-recommends-approaches / convergent-funnel posture, graphviz state machine, CSS HTML kit — all + off-pattern or inject AI ideas. + +### Surviving from legacy version + +- `brain-methods.csv` (technique library) → kept as a swappable asset (`{workflow.brain_methods}`). +- Anti-bias protocol (shift creative domain every ~10 ideas) and quantity goal (100+ collaboratively developed + ideas, stay in generative mode) → kept as facilitation posture, stripped of emoji/MANDATORY/SUCCESS-METRICS + ceremony. + +### Planned file tree + +``` +bmad-brainstorming/ +├── SKILL.md # Overview, Conventions, Activation, facilitation posture, technique flow, running log, finalize +├── customize.toml # output_dir/folder, brain_methods path, activation hooks, persistent_facts +├── references/ +│ └── headless.md # QUARANTINED autonomous mode (the one place the LLM brainstorms itself) +└── assets/ + └── brain-methods.csv # technique library (carried over) +``` + +Legacy files to remove on build: `workflow.md`, `template.md`, `steps/` (8 numbered step files). + +### Adversarial review (3 parallel lenses) + fixes applied + +Ran a Workflow with three review lenses (over-prescription, facilitation-integrity, BMad-conventions) against +the build. Fixes applied: + +- **[HIGH, fixed] Headless detection seam.** The "first message pre-supplies a topic and asks for artifacts" + trigger collided with a normal interactive opening ("brainstorm X and give me the HTML"), risking a present + human flipping the skill into self-generation. Made human-presence the dominant gate in both SKILL.md + activation step 4 and headless.md Detection; dropped the weak payload-shape trigger entirely. +- **[HIGH, fixed] `external_handoffs` declared-but-unused.** Wired it (not deleted) into SKILL.md + `## Producing Artifacts` and headless.md step 5, matching product-brief/prd; headless JSON now populated from + results. Scalar cross-check now 1:1 (every declared scalar referenced, every reference declared). +- **[MEDIUM, fixed] One-off idea exception loose/uncapped.** Tightened trigger to a *direct* ask (not "stuck" + / "what do you think") and capped recurrence (repeated reaching = pivot the technique). +- **[MEDIUM, fixed] Overview forward-reference + Running Log triple-imperative padding.** Trimmed both; added + "never with your own examples" glue at the provocation point and the headless-boundary clause to the scope + statement; ran `on_complete` in headless too. +- **Rejected (reviewers self-assessed as keepers):** ~10-idea rationale, self-contained-HTML constraint, + headless restatement (justified by reference-file self-containment under compaction), and the subjective + "spent / mined out" progression condition (a numeric gate would contradict "resist concluding"). + +### Technique library: 61 → 100, made context-lean via a script (user-requested) + +- **Schema change.** CSV is now `category, technique_name, description, detail`. `description` rewritten to a + terse ≤140-char *gist* that doubles as the index entry AND is enough to run most techniques (the LLM + reconstructs specifics from name + gist — user's insight). `detail` is an optional path (relative to the CSV + dir) to a per-technique instruction file, for techniques complex enough to warrant one — loaded only on + `show`. This absorbs the best of the "sharded files" idea for exactly the cases that need it. No separate + tagline column needed. +- **`scripts/brain.py`** (stdlib-only, PEP723, `uv run`, 15 passing tests in `scripts/tests/`): `categories` / + `list [--category]` / `show ""` (inlines detail file iff present) / `random`. Single source of truth; + index derived; never loads the whole catalog into main context. Decided script over subagent because + selecting/filtering is pure plumbing (quality-bar: scripts do plumbing, prompts do judgment); a subagent + would add latency without saving meaningful context now that the gist index is cheap. Noted one future case a + subagent helps: deep full-description matching across the whole library. +- **Context win:** whole-catalog load was ~4K tokens (61) and would have been ~6.5K at 100; now `categories` + ~0.1K, `list --category` ~0.3-0.6K, `show` ~0.07K each — flat as the library grows. The 100-technique CSV is + 14.6KB, *smaller* than the original 61-technique 16KB because gists replaced the long descriptions. +- **100 techniques, 13 categories** generated via a 13-agent parallel workflow (one flavored generator per + category: shorten existing + invent new), then curated by the parent to exactly 100 (dropped 7 cross-category + near-dups/one exact dup). New categories: `absurdist` (genuinely funny unstickers), `constraint`, + `speculative_future`. Spectrum spans silly→serious per user's "fun wild funny or super helpful." +- **Live detail-file demo:** `Six Thinking Hats` → `assets/techniques/six-thinking-hats.md` (full multi-round + method), proving the lazy-load path end to end. +- **"Invent a technique" option** added to `## Choosing Techniques` (pure prompt; logged as a technique; finalize + may offer to persist a keeper into `additional_techniques`). SKILL.md technique section + headless both rewired + to reach the library only through the script. Scalar consistency re-verified identical; scan-scripts passes. + +### Interactive tool techniques — a technique can BE a generated interactive HTML app (user breakthrough) + +The detail-file mechanism generalized into a new class: a technique whose "instructions" tell the LLM to +generate a bespoke, self-contained **interactive HTML tool** (CDN libs like three.js/d3 allowed) the user +manipulates directly, with results flowing back into the log. + +- **The copy-back bridge** (the load-bearing new mechanism, user-identified): the browser can't write to chat, + so every interactive tool carries a "Copy results for chat" button → `navigator.clipboard.writeText` of a + compact structured summary → user pastes it back → LLM captures it into `session.md` under that technique → + next technique. Stated ONCE in `references/interactive-tools.md` (shared build-and-bridge pattern), loaded only + when an interactive technique runs; each technique's detail file carries only its specifics. The Stance still + holds in chat — the tool externalizes the user's thinking, it does not start generating for them. Graceful + fallback to conversational facilitation if HTML can't run. +- **No per-technique python scripts needed:** the tool's logic lives in the generated HTML's JS (combinatorics, + genetic crossover, force/3D layouts run in-browser). `brain.py` stays the only script. This is simpler AND more + impressive (the artifact is shareable, runs anywhere) than server-side compute. +- **New `interactive` category (4 flagship tools)**, each with a detail spec in `assets/techniques/`: + Guided Mind Map (user's ask), Morphological Combinator (combinatorial-explosion + shuffle), Idea Genome + (genetic-algorithm breeding + lineage), Idea Constellation (force/3D star-map + gap-finding). Retired the + conversational Mind Mapping (structured) and Morphological Analysis (deep) — superseded by their interactive + versions; trimmed 2 weak ones (absurdist "Wizard Did It", wild "Villain's Master Plan") to hold exactly 100. +- **Two detail tiers now demoed:** simple (Six Thinking Hats — markdown only) and deep (the 4 interactive tools — + markdown spec for a generated JS app + the copy-back bridge). SKILL.md `## Choosing Techniques` routes to + `references/interactive-tools.md` + `show` when an interactive technique runs. scan-scripts passes; SKILL.md 92 + lines. Cruft (`.pytest_cache`, `__pycache__`) removed from the tree. + +### Open item for BMad (not a build defect) + +`.decision-log.md` trips the path-standards scanner (it flags any non-SKILL root `.md`). This is the builder's +canonical build memory, mandated at skill root by build-process.md and read by resume detection — moving it to +`references/` (the scanner's suggested fix) would be wrong. The reference skills (product-brief/prd) ship none, +so it is build-time-only: gitignore or delete it before release rather than "fixing" the lint. + +### Interactive tools: bundled & tested, not generated per-run (user-directed pivot) + +User flagged the live-generation approach as needlessly risky (the constellation demo shipped two engine-layer +typos that blanked the page) and conceptually wrong for one tool. Two decisions: + +- **Bundle the flagship interactive tools as tested HTML shells; inject data, never regenerate code.** The risk + lives entirely in the engine layer (WebGL/d3/event wiring/clipboard) which is identical every run; the + per-session value lives in the data + theming. So each tool now ships as a self-contained + `assets/techniques/.html` with a ` + + diff --git a/src/core-skills/bmad-brainstorming/assets/techniques/six-thinking-hats.md b/src/core-skills/bmad-brainstorming/assets/techniques/six-thinking-hats.md deleted file mode 100644 index eaaae9681..000000000 --- a/src/core-skills/bmad-brainstorming/assets/techniques/six-thinking-hats.md +++ /dev/null @@ -1,14 +0,0 @@ -# Six Thinking Hats — full method - -Edward de Bono's method: the group wears one "hat" at a time, so everyone explores the *same* mode of thinking together instead of arguing across modes. The power is in the discipline of separation — no defending while in White, no inventing while in Black. - -Facilitate the hats in sequence, one at a time. Keep the user generating within each hat before switching; announce each switch so the shift in thinking is shared. - -- **White — facts.** Only what is known and what is missing. Data, figures, gaps. No opinions, no interpretation. "What do we actually know? What don't we?" -- **Red — feelings.** Gut reactions, hunches, emotions — stated without justification. The one hat where "because I just feel it" is the whole point. "What's your gut say, no reasons needed?" -- **Yellow — benefits.** Best case, value, why it could work. Optimism with reasons. "If this went right, what would we gain?" -- **Black — risks.** Caution, flaws, what could fail. Critical judgment — but *only* under this hat, so it doesn't poison the rest. "Where does this break? What's the real danger?" -- **Green — new ideas.** Creativity, alternatives, provocations, growth from what the other hats surfaced. "Given all that — what else? What's the wild alternative?" -- **Blue — process.** Step back: summarize what came out, decide what matters, name the next move. Usually last; can also open the session to set the agenda. "What did we learn, and what do we do with it?" - -A common order is White → Red → Yellow → Black → Green → Blue, but adapt to the topic — lead with Red when feelings are running hot, or open with Blue to frame the session. Loop a hat if it is still producing. Capture each hat's output under its name in the running log. diff --git a/src/core-skills/bmad-brainstorming/references/finalize.md b/src/core-skills/bmad-brainstorming/references/finalize.md index 61b8cadea..16cb0684b 100644 --- a/src/core-skills/bmad-brainstorming/references/finalize.md +++ b/src/core-skills/bmad-brainstorming/references/finalize.md @@ -4,9 +4,9 @@ Load this when the user signals they're spent or the topic is mined out. `{doc_w ## Synthesis -This is the one place your own creative contribution is welcome. Run it in two moves, in order: +In Facilitator mode this is the one place your own creative contribution is welcome; in Creative Partner and Ideate-for-me you've been contributing all along, so just keep going. Run it in two moves, in order: -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. 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. Record the insights and chosen directions with `memlog.py append --type insight`. **Then run `python3 {skill-root}/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. @@ -17,10 +17,10 @@ Each artifact is a fresh, token-expensive generation, so the user opts in. Ask w **Delegate each artifact to a subagent.** By now the main context is full of the whole session — but the memlog holds everything, so the subagent doesn't need that context. Spawn one per requested artifact, telling it only: the spec below, the memlog path `{doc_workspace}/.memlog.md` (its sole source — read it in full), the output path, `{document_output_language}`, and "return ONLY the written file path." This keeps the heavy generation out of the main thread and proves the memlog is genuinely the canonical source. (Subagents can't spawn subagents — run these from here.) -- **Imaginative HTML keepsake (recommended default).** A single self-contained `brainstorm.html` in `{doc_workspace}` — a genuine creative artifact, not a report poured into a template. There is no template on purpose: let *this* session's subject, energy, and whimsy drive the visual language (a children's game and a supply-chain session should not look alike). Give each technique its own treatment, invent visualizations that fit the ideas (timelines, constellations, mind-maps, whatever the content wants), and render the synthesis as the climax. Inline all CSS and any JS; no external dependencies. Open it once complete. -- **Intent doc.** A succinct `brainstorm-intent.md` — the chosen and critical discoveries only, structured to drop straight into a downstream skill (`bmad-product-brief`, `bmad-prd`) as clean input, with none of the report's bloat. -- **Anything else they name** — a pitch, a one-pager, a task list — produced from the same source. +- **Imaginative HTML keepsake (recommended default).** A single self-contained `brainstorm.html` in `{doc_workspace}` — a genuine creative artifact, not a report poured into a template. There is no template on purpose: let *this* session's subject, energy, and whimsy drive the visual language (a children's game and a supply-chain session should not look alike). Give each technique its own treatment, invent visualizations that fit the ideas and techniques, and render the synthesis as the climax. Inline all CSS and any JS; no external dependencies. Open it once complete. +- **Intent doc.** A succinct `brainstorm-intent.md` — the chosen and critical discoveries only, structured to drop straight into a downstream skill (`bmad-product-brief`, `bmad-prd`) as clean input, with none of the report's bloat - token usage matters and it must really be on point. Confirm what the user wants to capture as the intent from the overall findings as there may be many divergent discoveries (unless in headless mode, then take your best educated stance). +- **Offer other options they might want from it also based on context** — a pitch, a one-pager, a task list — produced from the same source. These can be slide decks, html, markdown - again be creative and offer really interesting quality options based on perceived user needs while asking them also to offer any other ideas. -If the session used invented techniques, offer to save a keeper into `{workflow.additional_techniques}` via `bmad-customize`. +If the session used invented techniques, offer to save a keeper into `{workflow.additional_techniques}` via `bmad-customize` user preferences. -After producing what they chose, execute each `{workflow.external_handoffs}` entry to route artifacts beyond local files — invoke the named MCP tool, capture any returned URLs/IDs, surface them; skip and flag any unavailable tool, since the local files always exist. Then share the artifact paths (and any handoff destinations), invoke `bmad-help` to suggest where this leads next in the BMad ecosystem, and run `{workflow.on_complete}` if non-empty. +After producing what they chose, offer them ideas for deep dive brainstorming new sessions, offer to full extrapolate any ideas into an html report (autonomously brainstorm on their behalf), and most importantly: execute each `{workflow.external_handoffs}` instruction. Then share the artifact paths (and any handoff destinations), invoke `bmad-help` to suggest where this leads next in the BMad ecosystem, let them know if they feel a produced intent is detailed enough the could jump right into passing it to bmad-spec or any other analysis tool (outlined from bmad hlep) and run `{workflow.on_complete}` if non-empty. diff --git a/src/core-skills/bmad-brainstorming/references/mode-autonomous.md b/src/core-skills/bmad-brainstorming/references/mode-autonomous.md new file mode 100644 index 000000000..fcd12fe53 --- /dev/null +++ b/src/core-skills/bmad-brainstorming/references/mode-autonomous.md @@ -0,0 +1,10 @@ +# Mode: Ideate For Me + +The user handed you the topic and wants to see what you come up with on your own, then look at the result. You become the brainstormer — this is the one interactive mode where the ideas are yours to generate. + +- **Run a real divergent session yourself.** Pick and run techniques on your own (use `brain.py` as in `## Choosing Techniques`, but *you* choose — no menu for the user), capturing each idea to the memlog with `--type idea --by coach`, marking each technique switch with a `technique` entry, shifting the creative domain every ~10 ideas, aiming past 100. Push past the obvious. +- **Don't pepper the user with questions** — this is your run. One quick confirm of topic and goal up front is plenty. +- **When it's mined out, synthesize and produce the artifact.** Go to `## Wrap-Up` (`references/finalize.md`): record the insights, mark the memlog complete, and generate the imaginative HTML keepsake to show them. +- **Then, because a human is here, offer to keep going together.** They may want to push an idea further or react to what you found — if so, switch into **Facilitator** or **Creative Partner** (load that frame) and continue from the same memlog. + +This is the interactive sibling of headless mode (`references/headless.md`): the same self-generation, but a person is present to receive the output and may continue. headless is the no-human, returns-JSON runner; this one greets, presents, and hands off. diff --git a/src/core-skills/bmad-brainstorming/references/mode-facilitator.md b/src/core-skills/bmad-brainstorming/references/mode-facilitator.md new file mode 100644 index 000000000..1bee2205f --- /dev/null +++ b/src/core-skills/bmad-brainstorming/references/mode-facilitator.md @@ -0,0 +1,11 @@ +# Mode: Facilitator + +You are a forcing function for the user's creativity, never a source of ideas. The best version of this session ends with the user surprised by what *they* came up with — every idea in the memlog is theirs. + +- **You do not supply ideas.** Your moves are questions, provocations, constraints, and reflections that make *the user* generate, while you steer within the chosen technique. When the well looks dry, don't fill it — change the technique, shift the angle, or push harder. +- **The one exception:** if the user *directly asks* for an idea, give exactly one as a spark, then hand the pen back. Reaching for that repeatedly is the signal to change technique, not to keep feeding ideas. +- This holds for the whole generative session; it relaxes only during synthesis at wrap-up (`references/finalize.md`). + +Every idea you log is the user's, so no attribution is needed — log with `--type idea` (no `--by`). + +Go to `## Choosing Techniques`. diff --git a/src/core-skills/bmad-brainstorming/references/mode-partner.md b/src/core-skills/bmad-brainstorming/references/mode-partner.md new file mode 100644 index 000000000..847740010 --- /dev/null +++ b/src/core-skills/bmad-brainstorming/references/mode-partner.md @@ -0,0 +1,16 @@ +# Mode: Creative Partner + +You are still the facilitator — their creativity is the point, and they do the **majority** of the generating. But here you also play: you ride alongside and throw in your own ideas as sparks and yes-and fuel, so the two of you build a chain neither would alone. The energy is collaborative, not extractive — you feed off each other. + +**Set it up first.** Before you start, tell the user how this mode works and that they stay in control: they can **reject any idea you offer, ask you to help more or less, and tell you how to brainstorm** — a technique to try, a tone, a direction to chase. You're a partner they can steer, not a script. + +Hold the balance: + +- **Their fire, your kindling.** After you offer an idea, hand the pen back with a question. Never run a string of your own while they go quiet. +- **"Yes, and" is the default move.** Take what they just said, build it one rung higher, then dare them to top you. Make them *want* to outdo you. +- **Offer real alternatives**, not leading questions — a genuine idea they can mutate or reject, an opening, never a conclusion. +- **Watch the ratio.** If you've contributed more than they have over the last few exchanges, you've slipped toward doing it *for* them — pull back to questions and constraints. + +**Attribution is mandatory here.** Every idea entry records who it came from: `--by user` for theirs, `--by coach` for yours (e.g. `append --type idea --by coach --text "..."`). This keeps the record honest and lets the wrap-up hand *them* the mirror of what *they* generated. + +Go to `## Choosing Techniques`. diff --git a/src/core-skills/bmad-brainstorming/scripts/brain.py b/src/core-skills/bmad-brainstorming/scripts/brain.py index ab029d231..b023bd786 100644 --- a/src/core-skills/bmad-brainstorming/scripts/brain.py +++ b/src/core-skills/bmad-brainstorming/scripts/brain.py @@ -16,14 +16,18 @@ Commands: list --all the whole index at once — deliberate; large, avoid interactively show NAME [NAME ...] full gist for each, inlining its detail file if it has one random [--category C] [-n N] pick N at random (optionally within categories) + html --out PATH write the offline 'browse all' selection page to a file -`list` refuses to run with neither --category nor --all: dumping the full catalog -into context is a footgun, so it must be an explicit choice. +`list` refuses to run with neither --category nor --all, and `html` writes to a file +rather than stdout: dumping the full catalog into context is a footgun, so reaching the +whole library at once must always be an explicit, deliberate choice. Default output is lean text for an LLM to read; pass --json for structured output. """ import argparse import csv +import hashlib +import html import json import random import sys @@ -109,6 +113,555 @@ def fmt_show(rows: list[dict], csv_dir: Path, as_json: bool) -> str: return "\n\n".join(blocks) +def pretty(cat: str) -> str: + """Turn a category slug (e.g. 'speculative_future') into a display name.""" + return cat.replace("_", " ").replace("-", " ").title() + + +# --- card visuals: a crafted duotone icon + hue per category --------------- +# Each card shows its category's icon (drawn in `currentColor`, which the CSS sets +# to the category hue) plus a per-technique accent seeded by the technique name, so +# every card is unique while staying on-theme. Hues span the wheel so categories +# stay distinguishable; an unknown (user-added) category gets a hash-derived hue and +# a generic glyph, so custom catalogs still render. + +_HUES = { + "creative": "#6d5cf0", + "deep": "#4658c9", + "structured": "#3b6ea5", + "quantum": "#2b86d9", + "speculative_future": "#0fb5c9", + "collaborative": "#15a3a3", + "biomimetic": "#1f9d6b", + "constraint": "#d9882b", + "wild": "#e2562f", + "cultural": "#c75b39", + "theatrical": "#cf4d6f", + "absurdist": "#e0529c", + "introspective_delight": "#b15ad6", +} + +CHIP = '' + +_GLYPHS = { + # idea starburst + "creative": ( + '' + '' + '' + '' + '' + '' + '' + ), + # nested depth rings + "deep": ( + '' + '' + '' + '' + '' + ), + # 2x2 blocks, diagonal filled + "structured": ( + '' + '' + '' + '' + '' + ), + # atom + "quantum": ( + '' + '' + '' + '' + '' + '' + ), + # upward arrow to a twinkling star + "speculative_future": ( + '' + '' + '' + '' + '' + '' + ), + # three linked nodes + "collaborative": ( + '' + '' + '' + '' + '' + '' + '' + ), + # leaf + "biomimetic": ( + '' + '' + '' + '' + '' + '' + ), + # corner brackets framing a point + "constraint": ( + '' + '' + '' + '' + '' + ), + # lightning bolt + "wild": ( + '' + ), + # globe + "cultural": ( + '' + '' + '' + '' + '' + ), + # theatre mask + "theatrical": ( + '' + '' + '' + '' + ), + # off-kilter winking grin + "absurdist": ( + '' + '' + '' + '' + '' + '' + ), + # meditating figure + "introspective_delight": ( + '' + '' + '' + '' + ), +} + +_FALLBACK_GLYPH = ( + '' + '' + '' +) + + +def _hsl_hex(deg: int, s: float, lt: float) -> str: + import colorsys + + r, g, b = colorsys.hls_to_rgb((deg % 360) / 360, lt, s) + return "#%02x%02x%02x" % (round(r * 255), round(g * 255), round(b * 255)) + + +def category_style(cat: str) -> tuple[str, str]: + """(hue, glyph markup) for a category — crafted for the shipped set, derived for extras.""" + if cat in _HUES: + return _HUES[cat], _GLYPHS[cat] + deg = int(hashlib.md5(cat.encode("utf-8")).hexdigest(), 16) % 360 + return _hsl_hex(deg, 0.58, 0.52), _FALLBACK_GLYPH + + +# A deliberately chosen line-icon depicting each specific technique. Drawn in +# `currentColor` (the category hue), consistent 2px stroke. Shown beside the shared +# category icon on every card. Unknown (custom) techniques fall back to a neutral mark. +_S = '' +_FALLBACK_TECH = ( + '' +) + +_TECH_ICONS = { + # --- collaborative --- + "Yes And Building": '', + "Brain Writing Round Robin": _S + '', + "Random Stimulation": '', + "Role Playing": '', + "Ideation Relay Race": _S + '', + "Idea Hot Potato": '', + "Steal And Upgrade": _S + '', + "Fold The Paper": '', + # --- creative --- + "What If Scenarios": _S + '', + "Analogical Thinking": '', + "First Principles Thinking": '', + "Forced Relationships": '', + "Time Shifting": '', + "Metaphor Mapping": '', + "Cross-Pollination": _S + '', + "Concept Blending": '', + "Reverse Brainstorming": _S + '', + "Sensory Exploration": '', + # --- deep --- + "Five Whys": _S + '', + "Provocation Technique": _S + '', + "Assumption Reversal": _S + '', + "Question Storming": _S + '', + "Constraint Mapping": '', + "Failure Analysis": '', + "Emergent Thinking": '', + "Causal Loop Mapping": _S + '', + "Morphological Analysis": '', + "Laddering": _S + '', + # --- introspective_delight --- + "Inner Child Conference": '', + "Shadow Work Mining": '', + "Values Archaeology": '', + "Future Self Interview": _S + '', + "Body Wisdom Dialogue": '', + "Permission Giving": '', + "Secret Wish Confession": '', + "Mood Weather Report": '', + # --- structured --- + "SCAMPER Method": '', + "Six Thinking Hats": '', + "Decision Tree Mapping": _S + '', + "Solution Matrix": '', + "Trait Transfer": '', + "Lotus Blossom": '', + "Worst Possible Idea": _S + '', + "Disney Method": '', + "Starbursting": _S + '', + "Mind Mapping": _S + '', + "Crazy 8s": '', + # --- theatrical --- + "Time Travel Talk Show": '', + "Alien Anthropologist": '', + "Dream Fusion Laboratory": _S + '', + "Emotion Orchestra": _S + '', + "Parallel Universe Cafe": '', + "Persona Journey": '', + "Devil's Advocate Courtroom": _S + '', + # --- wild --- + "Chaos Engineering": _S + '', + "Guerrilla Gardening Ideas": _S + '', + "Pirate Code Brainstorm": '', + "Zombie Apocalypse Planning": '', + "Drunk History Retelling": _S + '', + "Anti-Solution": _S + '', + "Elemental Forces": _S + '', + # --- biomimetic --- + "Nature's Solutions": _S + '', + "Ecosystem Thinking": _S + '', + "Evolutionary Pressure": _S + '', + "Predator & Prey": '', + "Metamorphosis Stages": _S + '', + "Swarm Logic": _S + '', + # --- quantum --- + "Observer Effect": '', + "Entanglement Thinking": '', + "Superposition Collapse": _S + '', + "Relativity Frame Shift": '', + "Field Lines": _S + '', + "Quantum Tunneling": '', + # --- cultural --- + "Indigenous Wisdom": _S + '', + "Fusion Cuisine": _S + '', + "Ritual Innovation": _S + '', + "Mythic Frameworks": _S + '', + "Proverb Mining": _S + '', + "Ancestor Council": '', + "Trickster's Gambit": '', + # --- absurdist --- + "Villain's Monologue": _S + '', + "Explain It to a Golden Retriever": _S + '', + "Infomercial at 3AM": '', + "Drunk Uncle at Thanksgiving": _S + '', + "Cursed Genie": _S + '', + "Three Rounds of Stupid": _S + '', + # --- constraint --- + "Kill the Crown Jewel": _S + '', + "1000x Budget": '', + "Ship in 60 Minutes": '', + "The $0 Mandate": '', + "One Feature Only": _S + '', + "Crank the Dial to 11": '', + "Constraint Roulette": '', + # --- speculative_future --- + "Time Horizon Ladder": _S + '', + "Post-Scarcity Test": _S + '', + "Utopia vs Dystopia Split-Screen": '', + "Sci-Fi Artifact From the Future": '', + "Emerging Tech Collision": '', + "What-If-The-World-Changed Card Flip": '', + "Future Anthropologist Dig": _S + '', +} + + +def tech_icon(name: str) -> str: + """The hand-picked line-icon for a specific technique (neutral mark if unknown).""" + return _TECH_ICONS.get(name, _FALLBACK_TECH) + + +SELECTOR_TEMPLATE = r""" + + + + +BMad Method Brainstorming Selection + + + +
+

BMad Method Brainstorming Selection

+

Compose your session, hit Copy prompt, and paste it back into the chat to begin. {{TOTAL}}

+ +
+
+ Facilitation +
+ + + +
+
+
+ Techniques + Picked 0 + Random 0 + Invent 0 + AI picks 0 + Total 0 · 3–4 is the sweet spot +
+
+ +
+ + +
+ +
{{CHIPS}}
+ +
+
+{{BODY}} +
+
BMad Method · Brainstorming
+ + + +""" + + +def html_doc(rows: list[dict]) -> str: + """Render the self-contained 'browse all techniques' selection page from the catalog. + + Deterministic: categories sorted, techniques in file order — so the shipped asset can + be snapshot-tested against the CSV and never silently drifts out of sync. + """ + groups: dict[str, list[dict]] = {} + for r in rows: + groups.setdefault(r["category"], []).append(r) + sections, chips = [], [] + for cat in sorted(groups): + hue, glyph = category_style(cat) + disp = html.escape(pretty(cat)) + cards = [] + for r in groups[cat]: + name = html.escape(r["technique_name"]) + desc = html.escape(r["description"]) + cat_icon = ( + '' + f'{CHIP}{glyph}' + ) + t_icon = ( + '' + f'{CHIP}{tech_icon(r["technique_name"])}' + ) + cards.append( + '' + ) + chips.append(f'') + sections.append( + f'

{disp}{len(groups[cat])}

' + f'
{"".join(cards)}
' + ) + total = html.escape(f"{len(rows)} techniques across {len(groups)} categories.") + return ( + SELECTOR_TEMPLATE.replace("{{BODY}}", "\n".join(sections)) + .replace("{{CHIPS}}", "".join(chips)) + .replace("{{TOTAL}}", total) + ) + + def main(argv: list[str] | None = None) -> int: p = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) p.add_argument("--file", type=Path, default=DEFAULT_FILE, help="technique CSV (default: sibling assets/brain-methods.csv)") @@ -123,6 +676,8 @@ def main(argv: list[str] | None = None) -> int: pr = sub.add_parser("random", help="pick techniques at random") pr.add_argument("--category", action="append", help="restrict to a category (repeatable)") pr.add_argument("-n", type=int, default=1, help="how many (default 1)") + ph = sub.add_parser("html", help="write the offline 'browse all' selection page") + ph.add_argument("--out", help="file to write the page to (required; never prints the catalog)") args = p.parse_args(argv) if not args.file.is_file(): @@ -155,6 +710,18 @@ def main(argv: list[str] | None = None) -> int: print("# no techniques match", file=sys.stderr) return 1 print(fmt_list(random.sample(pool, min(args.n, len(pool))), args.json)) + elif args.cmd == "html": + if not args.out: + print( + "error: `html` needs --out PATH — it writes the selection page to a file and " + "never prints the catalog to stdout (which would defeat the point).", + file=sys.stderr, + ) + return 2 + out = Path(args.out) + out.parent.mkdir(parents=True, exist_ok=True) + out.write_text(html_doc(rows), encoding="utf-8") + print(f"wrote {out} ({len(rows)} techniques, {len(categories(rows))} categories)") return 0 diff --git a/src/core-skills/bmad-brainstorming/scripts/memlog.py b/src/core-skills/bmad-brainstorming/scripts/memlog.py index fac794892..bb4f4270b 100644 --- a/src/core-skills/bmad-brainstorming/scripts/memlog.py +++ b/src/core-skills/bmad-brainstorming/scripts/memlog.py @@ -44,13 +44,15 @@ The file shape (.memlog.md): - (decision) lead with one pre-categorized account; defer multi-account import Each entry may carry an optional `--type` — what KIND it is (idea, insight, question, -decision, technique, …) — rendered as a short inline tag. Omit it for a plain note. -The host skill names the vocabulary; the script does not. +decision, technique, …) — and an optional `--by` naming who it came from (e.g. `user`, +`coach`), for sessions where authorship matters. Both render into one short inline tag: +`(idea)`, `(idea by user)`, `(by coach)`. Omit them for a plain note. The host skill +names the vocabulary; the script does not. Commands: - init --workspace DIR [--field k=v ...] create the memlog (errors if it exists) - append --workspace DIR --text STR [--type T] append one entry at the end - set --workspace DIR --key K --value V set/replace a frontmatter field + init --workspace DIR [--field k=v ...] create the memlog (errors if it exists) + append --workspace DIR --text STR [--type T] [--by W] append one entry at the end + set --workspace DIR --key K --value V set/replace a frontmatter field The workspace is the run folder; the memlog is always {workspace}/.memlog.md. """ @@ -140,7 +142,10 @@ def cmd_append(args) -> int: path = memlog_path(args.workspace) meta, body = split(path.read_text(encoding="utf-8")) text = " ".join(args.text.split()) # collapse newlines/runs → one-line entry, no prose bloat - tag = f"({args.type}) " if args.type else "" + label = args.type or "" + if args.by: + label = f"{label} by {args.by}".strip() # attribution: "(idea by user)" / "(by coach)" + tag = f"({label}) " if label else "" entry = f"- {tag}{text}" body = (body.rstrip("\n") + "\n" + entry) if body.strip() else entry # always at the end touch(meta) @@ -172,6 +177,7 @@ def main(argv: list[str] | None = None) -> int: pa.add_argument("--workspace", required=True) pa.add_argument("--text", required=True) pa.add_argument("--type", help="entry kind, rendered as an inline tag") + pa.add_argument("--by", help="who the entry came from (e.g. user, coach); rendered into the tag") pa.set_defaults(func=cmd_append) pset = sub.add_parser("set", help="set a frontmatter field") diff --git a/src/core-skills/bmad-brainstorming/scripts/tests/test_brain.py b/src/core-skills/bmad-brainstorming/scripts/tests/test_brain.py index 125197f4c..c4fb17500 100644 --- a/src/core-skills/bmad-brainstorming/scripts/tests/test_brain.py +++ b/src/core-skills/bmad-brainstorming/scripts/tests/test_brain.py @@ -125,3 +125,40 @@ def test_random_respects_n_and_category(lib, capsys): def test_missing_file_returns_2(tmp_path): assert brain.main(["--file", str(tmp_path / "nope.csv"), "categories"]) == 2 + + +# --- html selection page ------------------------------------------------ + +def test_html_requires_out(lib, capsys): + # never dump the catalog to stdout — writing to a file is the whole point + assert brain.main(["--file", str(lib), "html"]) == 2 + assert "--out" in capsys.readouterr().err + + +def test_html_writes_selection_page(lib, tmp_path): + out = tmp_path / "sel.html" + assert brain.main(["--file", str(lib), "html", "--out", str(out)]) == 0 + doc = out.read_text(encoding="utf-8") + assert doc.startswith("") + assert "BMad Method Brainstorming Selection" in doc + for r in brain.load(lib): + assert r["technique_name"] in doc # every technique is selectable + assert ""yes and"" in doc # quotes in a description are escaped, not raw + + +def test_html_creates_missing_parent(lib, tmp_path): + out = tmp_path / "nested" / "deep" / "sel.html" + assert brain.main(["--file", str(lib), "html", "--out", str(out)]) == 0 + assert out.is_file() + + +def test_shipped_selector_is_in_sync_with_catalog(): + # foolproofing: if someone edits brain-methods.csv they must regenerate the page. + # Regenerate with: python3 brain.py html --out assets/brain-selector.html + asset = brain.DEFAULT_FILE.parent / "brain-selector.html" + assert asset.is_file(), "missing assets/brain-selector.html — generate it" + expected = brain.html_doc(brain.load(brain.DEFAULT_FILE)) + assert asset.read_text(encoding="utf-8") == expected, ( + "assets/brain-selector.html is stale; regenerate: " + "python3 brain.py html --out assets/brain-selector.html" + ) diff --git a/src/core-skills/bmad-brainstorming/scripts/tests/test_memlog.py b/src/core-skills/bmad-brainstorming/scripts/tests/test_memlog.py index 8f71defef..d16690dd1 100644 --- a/src/core-skills/bmad-brainstorming/scripts/tests/test_memlog.py +++ b/src/core-skills/bmad-brainstorming/scripts/tests/test_memlog.py @@ -44,10 +44,12 @@ def init(ws, **fields): assert memlog.main(argv) == 0 -def append(ws, text, type=None): +def append(ws, text, type=None, by=None): argv = ["append", "--workspace", ws, "--text", text] if type: argv += ["--type", type] + if by: + argv += ["--by", by] assert memlog.main(argv) == 0 @@ -142,6 +144,22 @@ def test_revisited_technique_is_just_a_later_entry(ws): ] +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", type="idea", by="user") + append(ws, "lid doubles as a plate", 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", type="idea") From e966110e711181bd5b84c84de1ec77bd4b7daa8d Mon Sep 17 00:00:00 2001 From: Brian Madison Date: Sun, 31 May 2026 23:40:57 -0500 Subject: [PATCH 7/8] bmad-brainstorming: fix code-review findings memlog.py - Parse frontmatter by the first line that is exactly `---`, so a `---` inside a topic/goal value no longer truncates the block, drops `status`, and breaks resume forever. Neutralize newlines in field values on render too. brain.py (selector page + CLI) - Composer: category toggles now define session scope; the text filter is a pure browse aid. checked() and the random pool both key off scope (offCats), so hidden cards are never silently copied and a stray filter term can't starve a random draw. - Clipboard: only show the "Copied!" banner when the copy actually succeeds; on failure show a warning and a prefilled prompt() so the text is never lost. - category_style: fall back to the neutral glyph instead of KeyError if the hue/glyph dicts ever desync. - random: clamp -n so a negative/oversized value returns cleanly instead of crashing. - --extra: merge a JSON overlay of additional_techniques into every command, so the browse page and category draws include custom techniques/categories as advertised. docs - SKILL.md: fix dangling `## Choosing Your Mode` anchor and the "Copy selection" button label; document --extra in the regen instructions. - mode-autonomous.md: persist the mode flip when handing off from autonomous, so a resume restores the new stance. - finalize.md: grammar/typo fixes (CodeRabbit). tests - Regression tests for the memlog `---` fix, --extra merge, negative -n, and the category fallback; regenerated the snapshot-tested selection page. Renamed the shadowing `type`/`l` locals flagged by CodeRabbit. 52 passing. --- src/core-skills/bmad-brainstorming/SKILL.md | 6 +- .../assets/brain-selector.html | 41 +++++++--- .../bmad-brainstorming/references/finalize.md | 2 +- .../references/mode-autonomous.md | 2 +- .../bmad-brainstorming/scripts/brain.py | 74 ++++++++++++++---- .../bmad-brainstorming/scripts/memlog.py | 20 +++-- .../scripts/tests/test_brain.py | 55 +++++++++++++- .../scripts/tests/test_memlog.py | 76 +++++++++++++------ 8 files changed, 216 insertions(+), 60 deletions(-) diff --git a/src/core-skills/bmad-brainstorming/SKILL.md b/src/core-skills/bmad-brainstorming/SKILL.md index 3d6bcb903..1d8bf780f 100644 --- a/src/core-skills/bmad-brainstorming/SKILL.md +++ b/src/core-skills/bmad-brainstorming/SKILL.md @@ -36,7 +36,7 @@ These hold the whole run, in every mode. They fight your defaults, so hold them - **Keep shifting the creative domain** — roughly every 5–10 turns (or every ~10 ideas when you're the one generating), usually by moving to the next technique. Divergence is a discipline, not a mood. - **While you're in dialogue (Facilitator and Creative Partner): one prompt per message, no multiple-choice menus.** Never stack questions into a wall the user reads instead of answers; never hand a menu that invites lazy picking — both pull them out of generating. The lone exceptions are the two up-front *process* choices (your mode, and the technique flow in `## Choosing Techniques`): *how* to run the session is the user's to pick; *what* to ideate never is. -What changes between modes is **who generates the ideas and how you relate to the user** — your stance. That is set by the mode the user picks (`## Choosing Your Mode`); load its frame and hold it alongside these. +What changes between modes is **who generates the ideas and how you relate to the user** — your stance. That is set by the mode the user picks (`## Choose How to Run It`); load its frame and hold it alongside these. ## Framing — The Memlog @@ -65,7 +65,7 @@ Two things get set before ideating: the **facilitation mode** (your stance) and **Primary — the composer page.** Send the user to it: - Default catalog → open `{skill-root}/assets/brain-selector.html`. -- Customized catalog (overridden `{workflow.brain_methods}` or any `{workflow.additional_techniques}`) → regenerate first, then open it: `python3 {skill-root}/scripts/brain.py --file {workflow.brain_methods} html --out {doc_workspace}/brain-selector.html`. +- Customized catalog (overridden `{workflow.brain_methods}` or any `{workflow.additional_techniques}`) → regenerate first, then open it. If there are `{workflow.additional_techniques}`, write them to a JSON file (a list of `{category, technique_name, description}` objects) and pass it as `--extra` so the page includes them too: `python3 {skill-root}/scripts/brain.py --file {workflow.brain_methods} [--extra {doc_workspace}/extra-techniques.json] html --out {doc_workspace}/brain-selector.html`. There they choose a facilitation mode, build a technique batch (tick cards, **+Random**, **+Invent**, **AI picks**), filter by category if they want, click **Copy prompt**, and paste it back. Read that pasted block: @@ -116,7 +116,7 @@ The library is large, so **never pull it whole into context.** The only way in i Once the user has chosen, run that flow and reach no further than the calls it names: - **Facilitator Chosen** — from the goal, your `{workflow.favorite_techniques}`, and the `categories` map, name a batch of 3–4; confirm exact names with a targeted `list --category` on only the one or two categories you are drawing from. Never enumerate the library to choose. -- **Browse** — hand the user the offline **selection page** so the catalog never enters context. With the default catalog, open the prebuilt `{skill-root}/assets/brain-selector.html`; with a customized catalog, regenerate first — `python3 {skill-root}/scripts/brain.py --file {workflow.brain_methods} html --out {doc_workspace}/brain-selector.html` — then open that. They tick techniques (3–4 is the sweet spot), click **Copy selection**, and paste the result back; that paste carries each technique's full category, name, and description, so you run them straight away — no `list` or `show` needed. +- **Browse** — hand the user the offline **selection page** so the catalog never enters context. With the default catalog, open the prebuilt `{skill-root}/assets/brain-selector.html`; with a customized catalog, regenerate first — `python3 {skill-root}/scripts/brain.py --file {workflow.brain_methods} [--extra {doc_workspace}/extra-techniques.json] html --out {doc_workspace}/brain-selector.html` (pass `--extra` when there are `{workflow.additional_techniques}`) — then open that. They tick techniques (3–4 is the sweet spot), click **Copy prompt**, and paste the result back; that paste carries each technique's full category, name, and description, so you run them straight away — no `list` or `show` needed. - **Category** — the user names 1–n categories; `random --category` draws the batch from them, so the progression varies session to session. No listing needed. - **Inventive Flow** — invent at least 3 techniques, announce the order before starting the first, and touch no script. Log each one's name + description so you can offer to save a keeper into `{workflow.additional_techniques}` (via `bmad-customize`) at wrap-up. diff --git a/src/core-skills/bmad-brainstorming/assets/brain-selector.html b/src/core-skills/bmad-brainstorming/assets/brain-selector.html index f814f7ca4..2ae6b5221 100644 --- a/src/core-skills/bmad-brainstorming/assets/brain-selector.html +++ b/src/core-skills/bmad-brainstorming/assets/brain-selector.html @@ -36,6 +36,7 @@ .chip:not(.on) { opacity:.9; } .banner { max-height:0; overflow:hidden; transition:max-height .25s ease, padding .22s ease, margin .22s ease; background:linear-gradient(90deg,var(--accent),#8275f2); color:#fff; border-radius:10px; font-weight:700; text-align:center; padding:0 14px; } .banner.show { max-height:64px; padding:13px 14px; margin-top:10px; } + .banner.fail { background:linear-gradient(90deg,var(--warn),#e0894a); } main { padding:18px 24px 60px; max-width:1120px; margin:0 auto; } section { margin:0 0 26px; } section > h2 { font-size:13px; text-transform:uppercase; letter-spacing:.08em; color:var(--c); margin:0 0 10px; border-bottom:1px solid color-mix(in srgb, var(--c) 24%, #e6e8f0); padding-bottom:6px; } @@ -131,13 +132,19 @@ chip.classList.toggle('on', on); if (on){ delete offCats[chip.dataset.cat]; } else { offCats[chip.dataset.cat] = true; } applyFilter(); + update(); // a toggled-off category leaves the session, so counts must refresh too }); }); boxes.forEach(function(b){ b.addEventListener('change', update); }); q.addEventListener('input', applyFilter); - function checked(){ return boxes.filter(function(b){ return b.checked; }); } + // A category toggled off (offCats) leaves the session entirely; the text filter is a + // transient browse aid that never changes what's selected. So both manual picks and the + // random pool key off offCats — never the search box — keeping the copied prompt in step + // with what the user sees, and never starving a random draw because of a stray filter term. + function inScope(b){ return !offCats[b.dataset.cat]; } + function checked(){ return boxes.filter(function(b){ return b.checked && inScope(b); }); } function update(){ $('pickN').textContent = checked().length; @@ -163,11 +170,7 @@ }); } - function visibleUnchecked(){ - return boxes.filter(function(b){ - return !b.checked && b.closest('label.tech').style.display !== 'none'; - }); - } + function randomPool(){ return boxes.filter(function(b){ return !b.checked && inScope(b); }); } function sample(arr, n){ var a = arr.slice(), out = []; @@ -177,7 +180,7 @@ function compose(){ var picks = checked().map(function(b){ return { n: b.dataset.name, c: b.dataset.cat, d: b.dataset.desc, r: false }; }); - var rnd = sample(visibleUnchecked(), state.rand).map(function(b){ return { n: b.dataset.name, c: b.dataset.cat, d: b.dataset.desc, r: true }; }); + var rnd = sample(randomPool(), state.rand).map(function(b){ return { n: b.dataset.name, c: b.dataset.cat, d: b.dataset.desc, r: true }; }); var techs = picks.concat(rnd); var L = ["Let's run my brainstorming session.", "", 'Facilitation mode: ' + state.mode + '.']; if (techs.length){ @@ -203,16 +206,32 @@ var ta = document.createElement('textarea'); ta.value = t; ta.style.position = 'fixed'; ta.style.opacity = '0'; document.body.appendChild(ta); ta.focus(); ta.select(); - try { document.execCommand('copy'); } catch(e){} + var ok = false; + try { ok = document.execCommand('copy'); } catch(e){ ok = false; } document.body.removeChild(ta); + return ok; + } + + function flash(ok, text){ + var b = $('banner'); + b.classList.toggle('fail', !ok); + b.innerHTML = ok + ? '✓ Copied! Now paste it into the chat to start your session.' + : '⚠ Couldn’t reach the clipboard — copy the text in the box, then paste it into the chat.'; + b.classList.add('show'); + setTimeout(function(){ b.classList.remove('show'); }, 4500); + // Last resort on a hard failure: a prefilled, selectable prompt so the text is never lost. + if (!ok){ window.prompt('Copy this, then paste it into the chat:', text); } } $('copy').addEventListener('click', function(){ var text = compose(); - var show = function(){ var b = $('banner'); b.classList.add('show'); setTimeout(function(){ b.classList.remove('show'); }, 4500); }; if (navigator.clipboard && navigator.clipboard.writeText){ - navigator.clipboard.writeText(text).then(show, function(){ fallbackCopy(text); show(); }); - } else { fallbackCopy(text); show(); } + navigator.clipboard.writeText(text).then( + function(){ flash(true, text); }, + function(){ flash(fallbackCopy(text), text); } + ); + } else { flash(fallbackCopy(text), text); } }); update(); diff --git a/src/core-skills/bmad-brainstorming/references/finalize.md b/src/core-skills/bmad-brainstorming/references/finalize.md index 16cb0684b..a920a4f66 100644 --- a/src/core-skills/bmad-brainstorming/references/finalize.md +++ b/src/core-skills/bmad-brainstorming/references/finalize.md @@ -23,4 +23,4 @@ Each artifact is a fresh, token-expensive generation, so the user opts in. Ask w If the session used invented techniques, offer to save a keeper into `{workflow.additional_techniques}` via `bmad-customize` user preferences. -After producing what they chose, offer them ideas for deep dive brainstorming new sessions, offer to full extrapolate any ideas into an html report (autonomously brainstorm on their behalf), and most importantly: execute each `{workflow.external_handoffs}` instruction. Then share the artifact paths (and any handoff destinations), invoke `bmad-help` to suggest where this leads next in the BMad ecosystem, let them know if they feel a produced intent is detailed enough the could jump right into passing it to bmad-spec or any other analysis tool (outlined from bmad hlep) and run `{workflow.on_complete}` if non-empty. +After producing what they chose, offer them ideas for deep-dive brainstorming new sessions, offer to fully extrapolate any ideas into an html report (autonomously brainstorm on their behalf), and most importantly: execute each `{workflow.external_handoffs}` instruction. Then share the artifact paths (and any handoff destinations), invoke `bmad-help` to suggest where this leads next in the BMad ecosystem, let them know if they feel a produced intent is detailed enough they could jump right into passing it to bmad-spec or any other analysis tool (outlined from bmad-help) and run `{workflow.on_complete}` if non-empty. diff --git a/src/core-skills/bmad-brainstorming/references/mode-autonomous.md b/src/core-skills/bmad-brainstorming/references/mode-autonomous.md index fcd12fe53..f92a38e47 100644 --- a/src/core-skills/bmad-brainstorming/references/mode-autonomous.md +++ b/src/core-skills/bmad-brainstorming/references/mode-autonomous.md @@ -5,6 +5,6 @@ The user handed you the topic and wants to see what you come up with on your own - **Run a real divergent session yourself.** Pick and run techniques on your own (use `brain.py` as in `## Choosing Techniques`, but *you* choose — no menu for the user), capturing each idea to the memlog with `--type idea --by coach`, marking each technique switch with a `technique` entry, shifting the creative domain every ~10 ideas, aiming past 100. Push past the obvious. - **Don't pepper the user with questions** — this is your run. One quick confirm of topic and goal up front is plenty. - **When it's mined out, synthesize and produce the artifact.** Go to `## Wrap-Up` (`references/finalize.md`): record the insights, mark the memlog complete, and generate the imaginative HTML keepsake to show them. -- **Then, because a human is here, offer to keep going together.** They may want to push an idea further or react to what you found — if so, switch into **Facilitator** or **Creative Partner** (load that frame) and continue from the same memlog. +- **Then, because a human is here, offer to keep going together.** They may want to push an idea further or react to what you found — if so, switch into **Facilitator** or **Creative Partner** (load that frame), **record the switch in the memlog** so a resume restores the new stance — `python3 {skill-root}/scripts/memlog.py set --workspace {doc_workspace} --key mode --value ` — and continue from the same memlog. This is the interactive sibling of headless mode (`references/headless.md`): the same self-generation, but a person is present to receive the output and may continue. headless is the no-human, returns-JSON runner; this one greets, presents, and hands off. diff --git a/src/core-skills/bmad-brainstorming/scripts/brain.py b/src/core-skills/bmad-brainstorming/scripts/brain.py index b023bd786..02d428c04 100644 --- a/src/core-skills/bmad-brainstorming/scripts/brain.py +++ b/src/core-skills/bmad-brainstorming/scripts/brain.py @@ -22,6 +22,10 @@ Commands: rather than stdout: dumping the full catalog into context is a footgun, so reaching the whole library at once must always be an explicit, deliberate choice. +`--extra PATH` merges a JSON overlay of additional techniques (customize.toml's +`additional_techniques`) into every command, so custom techniques and whole new +categories are first-class everywhere — including the browse page and category draws. + Default output is lean text for an LLM to read; pass --json for structured output. """ import argparse @@ -46,6 +50,24 @@ def load(file: Path) -> list[dict]: return rows +def load_extra(file: Path) -> list[dict]: + """Merge-in techniques from a JSON overlay — a list of + {category, technique_name, description[, detail]} objects. This is how + customize.toml's `additional_techniques` become first-class across *every* + subcommand (categories/list/random/show/html), so the browse page and + category draws include them too, not just the in-chat flows.""" + data = json.loads(file.read_text(encoding="utf-8")) + rows = [] + for item in data: + rows.append({ + "category": str(item.get("category", "")).strip(), + "technique_name": str(item.get("technique_name", "")).strip(), + "description": str(item.get("description", "")).strip(), + "detail": str(item.get("detail") or "").strip(), + }) + return rows + + def categories(rows: list[dict]) -> list[tuple[str, int]]: counts: dict[str, int] = {} for r in rows: @@ -269,7 +291,7 @@ def _hsl_hex(deg: int, s: float, lt: float) -> str: def category_style(cat: str) -> tuple[str, str]: """(hue, glyph markup) for a category — crafted for the shipped set, derived for extras.""" if cat in _HUES: - return _HUES[cat], _GLYPHS[cat] + return _HUES[cat], _GLYPHS.get(cat, _FALLBACK_GLYPH) deg = int(hashlib.md5(cat.encode("utf-8")).hexdigest(), 16) % 360 return _hsl_hex(deg, 0.58, 0.52), _FALLBACK_GLYPH @@ -443,6 +465,7 @@ SELECTOR_TEMPLATE = r""" .chip:not(.on) { opacity:.9; } .banner { max-height:0; overflow:hidden; transition:max-height .25s ease, padding .22s ease, margin .22s ease; background:linear-gradient(90deg,var(--accent),#8275f2); color:#fff; border-radius:10px; font-weight:700; text-align:center; padding:0 14px; } .banner.show { max-height:64px; padding:13px 14px; margin-top:10px; } + .banner.fail { background:linear-gradient(90deg,var(--warn),#e0894a); } main { padding:18px 24px 60px; max-width:1120px; margin:0 auto; } section { margin:0 0 26px; } section > h2 { font-size:13px; text-transform:uppercase; letter-spacing:.08em; color:var(--c); margin:0 0 10px; border-bottom:1px solid color-mix(in srgb, var(--c) 24%, #e6e8f0); padding-bottom:6px; } @@ -526,13 +549,19 @@ SELECTOR_TEMPLATE = r""" chip.classList.toggle('on', on); if (on){ delete offCats[chip.dataset.cat]; } else { offCats[chip.dataset.cat] = true; } applyFilter(); + update(); // a toggled-off category leaves the session, so counts must refresh too }); }); boxes.forEach(function(b){ b.addEventListener('change', update); }); q.addEventListener('input', applyFilter); - function checked(){ return boxes.filter(function(b){ return b.checked; }); } + // A category toggled off (offCats) leaves the session entirely; the text filter is a + // transient browse aid that never changes what's selected. So both manual picks and the + // random pool key off offCats — never the search box — keeping the copied prompt in step + // with what the user sees, and never starving a random draw because of a stray filter term. + function inScope(b){ return !offCats[b.dataset.cat]; } + function checked(){ return boxes.filter(function(b){ return b.checked && inScope(b); }); } function update(){ $('pickN').textContent = checked().length; @@ -558,11 +587,7 @@ SELECTOR_TEMPLATE = r""" }); } - function visibleUnchecked(){ - return boxes.filter(function(b){ - return !b.checked && b.closest('label.tech').style.display !== 'none'; - }); - } + function randomPool(){ return boxes.filter(function(b){ return !b.checked && inScope(b); }); } function sample(arr, n){ var a = arr.slice(), out = []; @@ -572,7 +597,7 @@ SELECTOR_TEMPLATE = r""" function compose(){ var picks = checked().map(function(b){ return { n: b.dataset.name, c: b.dataset.cat, d: b.dataset.desc, r: false }; }); - var rnd = sample(visibleUnchecked(), state.rand).map(function(b){ return { n: b.dataset.name, c: b.dataset.cat, d: b.dataset.desc, r: true }; }); + var rnd = sample(randomPool(), state.rand).map(function(b){ return { n: b.dataset.name, c: b.dataset.cat, d: b.dataset.desc, r: true }; }); var techs = picks.concat(rnd); var L = ["Let's run my brainstorming session.", "", 'Facilitation mode: ' + state.mode + '.']; if (techs.length){ @@ -598,16 +623,32 @@ SELECTOR_TEMPLATE = r""" var ta = document.createElement('textarea'); ta.value = t; ta.style.position = 'fixed'; ta.style.opacity = '0'; document.body.appendChild(ta); ta.focus(); ta.select(); - try { document.execCommand('copy'); } catch(e){} + var ok = false; + try { ok = document.execCommand('copy'); } catch(e){ ok = false; } document.body.removeChild(ta); + return ok; + } + + function flash(ok, text){ + var b = $('banner'); + b.classList.toggle('fail', !ok); + b.innerHTML = ok + ? '✓ Copied! Now paste it into the chat to start your session.' + : '⚠ Couldn’t reach the clipboard — copy the text in the box, then paste it into the chat.'; + b.classList.add('show'); + setTimeout(function(){ b.classList.remove('show'); }, 4500); + // Last resort on a hard failure: a prefilled, selectable prompt so the text is never lost. + if (!ok){ window.prompt('Copy this, then paste it into the chat:', text); } } $('copy').addEventListener('click', function(){ var text = compose(); - var show = function(){ var b = $('banner'); b.classList.add('show'); setTimeout(function(){ b.classList.remove('show'); }, 4500); }; if (navigator.clipboard && navigator.clipboard.writeText){ - navigator.clipboard.writeText(text).then(show, function(){ fallbackCopy(text); show(); }); - } else { fallbackCopy(text); show(); } + navigator.clipboard.writeText(text).then( + function(){ flash(true, text); }, + function(){ flash(fallbackCopy(text), text); } + ); + } else { flash(fallbackCopy(text), text); } }); update(); @@ -665,6 +706,7 @@ def html_doc(rows: list[dict]) -> str: def main(argv: list[str] | None = None) -> int: p = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) p.add_argument("--file", type=Path, default=DEFAULT_FILE, help="technique CSV (default: sibling assets/brain-methods.csv)") + p.add_argument("--extra", type=Path, help="JSON overlay of additional techniques (customize.toml additional_techniques), merged into every command") p.add_argument("--json", action="store_true", help="emit structured JSON instead of lean text") sub = p.add_subparsers(dest="cmd", required=True) sub.add_parser("categories", help="list category names + counts") @@ -684,6 +726,11 @@ def main(argv: list[str] | None = None) -> int: print(f"error: technique file not found: {args.file}", file=sys.stderr) return 2 rows = load(args.file) + if args.extra: + if not args.extra.is_file(): + print(f"error: --extra file not found: {args.extra}", file=sys.stderr) + return 2 + rows += load_extra(args.extra) csv_dir = args.file.resolve().parent if args.cmd == "categories": @@ -709,7 +756,8 @@ def main(argv: list[str] | None = None) -> int: if not pool: print("# no techniques match", file=sys.stderr) return 1 - print(fmt_list(random.sample(pool, min(args.n, len(pool))), args.json)) + n = max(0, min(args.n, len(pool))) # clamp: never crash on a negative or oversized -n + print(fmt_list(random.sample(pool, n), args.json)) elif args.cmd == "html": if not args.out: print( diff --git a/src/core-skills/bmad-brainstorming/scripts/memlog.py b/src/core-skills/bmad-brainstorming/scripts/memlog.py index bb4f4270b..99ec13225 100644 --- a/src/core-skills/bmad-brainstorming/scripts/memlog.py +++ b/src/core-skills/bmad-brainstorming/scripts/memlog.py @@ -75,20 +75,28 @@ def memlog_path(workspace: str) -> Path: def split(text: str) -> tuple[dict, str]: - """Return (frontmatter dict in source order, body str). Frontmatter is plain key: value.""" - if not text.startswith("---"): + """Return (frontmatter dict in source order, body str). Frontmatter is plain key: value. + + The closing fence is the first line that is *exactly* `---`, so a `---` inside a + field value (topic/goal are free user text) never truncates the frontmatter. + """ + lines = text.splitlines() + if not lines or lines[0] != "---": raise ValueError(".memlog.md has no frontmatter") - _, fm, body = text.split("---", 2) + end = next((i for i in range(1, len(lines)) if lines[i] == "---"), None) + if end is None: + raise ValueError(".memlog.md frontmatter is not terminated") meta: dict[str, str] = {} - for line in fm.strip().splitlines(): + for line in lines[1:end]: if ":" in line: k, v = line.split(":", 1) meta[k.strip()] = v.strip() - return meta, body.lstrip("\n") + return meta, "\n".join(lines[end + 1:]).lstrip("\n") def render(meta: dict, body: str) -> str: - fm = "\n".join(f"{k}: {v}" for k, v in meta.items()) + # Neutralize newlines in values so a multi-line field can't break the fence on re-read. + fm = "\n".join(f"{k}: {' '.join(str(v).splitlines())}" for k, v in meta.items()) return "---\n" + fm + "\n---\n\n" + body.rstrip("\n") + "\n" diff --git a/src/core-skills/bmad-brainstorming/scripts/tests/test_brain.py b/src/core-skills/bmad-brainstorming/scripts/tests/test_brain.py index c4fb17500..5da6955a6 100644 --- a/src/core-skills/bmad-brainstorming/scripts/tests/test_brain.py +++ b/src/core-skills/bmad-brainstorming/scripts/tests/test_brain.py @@ -120,7 +120,13 @@ def test_random_respects_n_and_category(lib, capsys): brain.main(["--file", str(lib), "random", "--category", "wild", "-n", "5"]) lines = capsys.readouterr().out.strip().splitlines() assert len(lines) == 2 # only 2 wild exist, n capped - assert all(l.startswith("wild\t") for l in lines) + assert all(line.startswith("wild\t") for line in lines) + + +def test_random_negative_n_does_not_crash(lib, capsys): + # a negative -n is clamped to 0, not passed to random.sample (which would raise) + assert brain.main(["--file", str(lib), "random", "-n", "-1"]) == 0 + assert capsys.readouterr().out.strip() == "" def test_missing_file_returns_2(tmp_path): @@ -152,6 +158,53 @@ def test_html_creates_missing_parent(lib, tmp_path): assert out.is_file() +# --- --extra overlay (customize.toml additional_techniques) ------------- + +EXTRA = ( + '[{"category": "domain-specific", "technique_name": "Regulatory Inversion", ' + '"description": "Start from the compliance constraint and brainstorm what it unlocks."}, ' + '{"category": "wild", "technique_name": "Extra Wild One", "description": "An added wild method."}]' +) + + +@pytest.fixture +def extra(tmp_path): + p = tmp_path / "extra.json" + p.write_text(EXTRA, encoding="utf-8") + return p + + +def test_extra_merges_into_categories(lib, extra, capsys): + brain.main(["--file", str(lib), "--extra", str(extra), "categories"]) + out = capsys.readouterr().out + assert "domain-specific\t1" in out # a brand-new category appears + assert "wild\t3" in out # the extra wild one is counted alongside the shipped two + + +def test_extra_appears_in_list_and_random(lib, extra, capsys): + brain.main(["--file", str(lib), "--extra", str(extra), "list", "--category", "domain-specific"]) + assert "Regulatory Inversion" in capsys.readouterr().out + + +def test_extra_is_first_class_in_html(lib, extra, tmp_path): + out = tmp_path / "sel.html" + assert brain.main(["--file", str(lib), "--extra", str(extra), "html", "--out", str(out)]) == 0 + doc = out.read_text(encoding="utf-8") + # custom technique is selectable and its new category renders without crashing (fallback glyph/hue) + assert "Regulatory Inversion" in doc + assert "Domain Specific" in doc + + +def test_extra_missing_file_returns_2(lib, tmp_path): + assert brain.main(["--file", str(lib), "--extra", str(tmp_path / "nope.json"), "categories"]) == 2 + + +def test_unknown_category_style_uses_fallback_glyph(): + hue, glyph = brain.category_style("totally-made-up-category") + assert hue.startswith("#") and len(hue) == 7 # valid derived hex + assert glyph == brain._FALLBACK_GLYPH + + def test_shipped_selector_is_in_sync_with_catalog(): # foolproofing: if someone edits brain-methods.csv they must regenerate the page. # Regenerate with: python3 brain.py html --out assets/brain-selector.html diff --git a/src/core-skills/bmad-brainstorming/scripts/tests/test_memlog.py b/src/core-skills/bmad-brainstorming/scripts/tests/test_memlog.py index d16690dd1..5e7813829 100644 --- a/src/core-skills/bmad-brainstorming/scripts/tests/test_memlog.py +++ b/src/core-skills/bmad-brainstorming/scripts/tests/test_memlog.py @@ -44,10 +44,10 @@ def init(ws, **fields): assert memlog.main(argv) == 0 -def append(ws, text, type=None, by=None): +def append(ws, text, entry_type=None, by=None): argv = ["append", "--workspace", ws, "--text", text] - if type: - argv += ["--type", type] + if entry_type: + argv += ["--type", entry_type] if by: argv += ["--by", by] assert memlog.main(argv) == 0 @@ -98,16 +98,16 @@ def test_append_lands_at_end_in_order(ws): def test_no_sections_or_headings_ever(ws): init(ws) - append(ws, "started foo", type="technique") - append(ws, "an idea", type="idea") - append(ws, "started bar", type="technique") + 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", type="idea") - append(ws, "how do we handle stampede?", type="question") + 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 @@ -128,12 +128,12 @@ def test_append_collapses_newlines_into_one_line(ws): 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", type="technique") - append(ws, "magnetic latch", type="idea") - append(ws, "started Six Hats", type="technique") - append(ws, "stale data risk", type="idea") - append(ws, "started SCAMPER", type="technique") # back to SCAMPER — just appended again - append(ws, "stackable tiers", type="idea") + 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", @@ -147,8 +147,8 @@ def test_revisited_technique_is_just_a_later_entry(ws): 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", type="idea", by="user") - append(ws, "lid doubles as a plate", type="idea", by="coach") + 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 @@ -162,10 +162,10 @@ def test_by_without_type_renders_alone(ws): def test_heterogeneous_entry_types_coexist(ws): init(ws) - append(ws, "an idea", type="idea") - append(ws, "an open question", type="question") - append(ws, "a decision we made", type="decision") - append(ws, "user wants mobile-first", type="direction") + 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 @@ -181,7 +181,7 @@ def test_set_flips_status(ws): def test_set_preserves_body(ws): init(ws) - append(ws, "keep me", type="idea") + 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" @@ -205,7 +205,7 @@ def test_updated_stays_last(ws): def test_roundtrip_render_is_stable(ws): init(ws) - append(ws, "one", type="idea") + append(ws, "one", entry_type="idea") first = read(ws) meta, body = memlog.split(first) assert memlog.render(meta, body) == first @@ -213,14 +213,42 @@ def test_roundtrip_render_is_stable(ws): def test_commas_in_field_survive(ws): init(ws, topic="cars, trains, and planes") - append(ws, "z", type="idea") + 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", type="idea") + append(ws, "x", entry_type="idea") out = json.loads(capsys.readouterr().out.strip().splitlines()[-1]) assert out["ok"] is True assert out["status"] == "active" From d0f47de0ef23ad311e984500db5da781ea5e6018 Mon Sep 17 00:00:00 2001 From: Brian Madison Date: Mon, 1 Jun 2026 10:15:04 -0500 Subject: [PATCH 8/8] bmad-brainstorming: composer header polish + dark mode MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Center header content (.hwrap) so it aligns with the card column on wide screens. - Replace the text filter with jump-nav: category chips smooth-scroll to their section (offset for the sticky header); drop the category exclude-toggle, so Random/AI draw from the whole catalog. - Fix narrow-screen crowding between the chips and the Copy prompt button. - Move Copy prompt to the end of the Techniques row, anchored to the Total readout. - Add a per-mode hint line that explains the selected facilitation stance. - Dark mode: refactor all colors to CSS variables + a dark palette, with a header toggle (☾/☀) that defaults to system preference and persists in localStorage; an inline head script applies the theme before first paint to avoid a flash. Category hues are lifted toward white on dark surfaces to stay legible. Regenerated the snapshot-tested selection page; SKILL.md wording updated (chips are jump-nav, not a filter). 52 Python tests passing. --- src/core-skills/bmad-brainstorming/SKILL.md | 2 +- .../assets/brain-selector.html | 141 ++++++++++------- .../bmad-brainstorming/scripts/brain.py | 143 +++++++++++------- 3 files changed, 176 insertions(+), 110 deletions(-) diff --git a/src/core-skills/bmad-brainstorming/SKILL.md b/src/core-skills/bmad-brainstorming/SKILL.md index 1d8bf780f..9552d1ee5 100644 --- a/src/core-skills/bmad-brainstorming/SKILL.md +++ b/src/core-skills/bmad-brainstorming/SKILL.md @@ -67,7 +67,7 @@ Two things get set before ideating: the **facilitation mode** (your stance) and - Default catalog → open `{skill-root}/assets/brain-selector.html`. - Customized catalog (overridden `{workflow.brain_methods}` or any `{workflow.additional_techniques}`) → regenerate first, then open it. If there are `{workflow.additional_techniques}`, write them to a JSON file (a list of `{category, technique_name, description}` objects) and pass it as `--extra` so the page includes them too: `python3 {skill-root}/scripts/brain.py --file {workflow.brain_methods} [--extra {doc_workspace}/extra-techniques.json] html --out {doc_workspace}/brain-selector.html`. -There they choose a facilitation mode, build a technique batch (tick cards, **+Random**, **+Invent**, **AI picks**), filter by category if they want, click **Copy prompt**, and paste it back. Read that pasted block: +There they choose a facilitation mode, build a technique batch (tick cards, **+Random**, **+Invent**, **AI picks**), jump to a category via the chips if they want, click **Copy prompt**, and paste it back. Read that pasted block: - the **`Facilitation mode:`** line → the mode; - the **listed techniques** — full category, name, and description are included, some tagged `(random pick)` → that is the batch; run them as given, no `list`/`show` needed; diff --git a/src/core-skills/bmad-brainstorming/assets/brain-selector.html b/src/core-skills/bmad-brainstorming/assets/brain-selector.html index 2ae6b5221..e7d508a76 100644 --- a/src/core-skills/bmad-brainstorming/assets/brain-selector.html +++ b/src/core-skills/bmad-brainstorming/assets/brain-selector.html @@ -4,58 +4,88 @@ BMad Method Brainstorming Selection +
-

BMad Method Brainstorming Selection

+
+
+

BMad Method Brainstorming Selection

+ +

Compose your session, hit Copy prompt, and paste it back into the chat to begin. 100 techniques across 13 categories.

@@ -66,6 +96,7 @@
+
Techniques @@ -74,16 +105,17 @@ Invent 0 AI picks 0 Total 0 · 3–4 is the sweet spot +
- - + Jump to +
-
+

Absurdist6

@@ -106,15 +138,31 @@ var $ = function(id){ return document.getElementById(id); }; var all = Array.prototype.slice; var boxes = all.call(document.querySelectorAll('input[type=checkbox]')); - var q = $('q'); + var header = document.querySelector('header'); + var sections = all.call(document.querySelectorAll('section')); var state = { mode: 'Facilitator', rand: 0, inv: 0, ai: 0 }; - var offCats = {}; + var MODE_HINTS = { + 'Facilitator': 'A forcing function for your ideas — I prompt and push, but never supply them.', + 'Creative Partner': 'We riff together — I facilitate and add ideas too, each logged as yours or mine.', + 'Ideate for me': 'I run the whole session myself, then show you the result and offer to keep going.' + }; + function setHint(){ $('modehint').textContent = MODE_HINTS[state.mode] || ''; } + + var themeBtn = $('theme'); + function setThemeIcon(){ themeBtn.textContent = document.documentElement.getAttribute('data-theme') === 'dark' ? '☀' : '☾'; } + themeBtn.addEventListener('click', function(){ + var next = document.documentElement.getAttribute('data-theme') === 'dark' ? 'light' : 'dark'; + document.documentElement.setAttribute('data-theme', next); + try { localStorage.setItem('bmad-theme', next); } catch(e){} + setThemeIcon(); + }); all.call(document.querySelectorAll('.mode')).forEach(function(b){ b.addEventListener('click', function(){ all.call(document.querySelectorAll('.mode')).forEach(function(m){ m.classList.remove('on'); }); b.classList.add('on'); state.mode = b.dataset.mode; + setHint(); }); }); @@ -126,25 +174,21 @@ }); }); + // Category chips are jump-nav: click one to smooth-scroll its section into view, + // offsetting by the sticky header's height so the heading isn't hidden beneath it. all.call(document.querySelectorAll('.chip')).forEach(function(chip){ chip.addEventListener('click', function(){ - var on = !chip.classList.contains('on'); - chip.classList.toggle('on', on); - if (on){ delete offCats[chip.dataset.cat]; } else { offCats[chip.dataset.cat] = true; } - applyFilter(); - update(); // a toggled-off category leaves the session, so counts must refresh too + var sec = null; + for (var i = 0; i < sections.length; i++){ if (sections[i].dataset.cat === chip.dataset.cat){ sec = sections[i]; break; } } + if (!sec){ return; } + var top = sec.getBoundingClientRect().top + window.pageYOffset - header.offsetHeight - 8; + window.scrollTo({ top: top, behavior: 'smooth' }); }); }); boxes.forEach(function(b){ b.addEventListener('change', update); }); - q.addEventListener('input', applyFilter); - // A category toggled off (offCats) leaves the session entirely; the text filter is a - // transient browse aid that never changes what's selected. So both manual picks and the - // random pool key off offCats — never the search box — keeping the copied prompt in step - // with what the user sees, and never starving a random draw because of a stray filter term. - function inScope(b){ return !offCats[b.dataset.cat]; } - function checked(){ return boxes.filter(function(b){ return b.checked && inScope(b); }); } + function checked(){ return boxes.filter(function(b){ return b.checked; }); } function update(){ $('pickN').textContent = checked().length; @@ -157,20 +201,7 @@ t.classList.toggle('warn', total > 5); } - function applyFilter(){ - var s = q.value.trim().toLowerCase(); - all.call(document.querySelectorAll('label.tech')).forEach(function(l){ - var cat = l.querySelector('input').dataset.cat; - var hay = (l.textContent + ' ' + cat).toLowerCase(); - l.style.display = (!offCats[cat] && (!s || hay.indexOf(s) > -1)) ? '' : 'none'; - }); - all.call(document.querySelectorAll('section')).forEach(function(sec){ - var any = all.call(sec.querySelectorAll('label.tech')).some(function(l){ return l.style.display !== 'none'; }); - sec.style.display = (!offCats[sec.dataset.cat] && any) ? '' : 'none'; - }); - } - - function randomPool(){ return boxes.filter(function(b){ return !b.checked && inScope(b); }); } + function randomPool(){ return boxes.filter(function(b){ return !b.checked; }); } function sample(arr, n){ var a = arr.slice(), out = []; @@ -234,6 +265,8 @@ } else { flash(fallbackCopy(text), text); } }); + setHint(); + setThemeIcon(); update(); })(); diff --git a/src/core-skills/bmad-brainstorming/scripts/brain.py b/src/core-skills/bmad-brainstorming/scripts/brain.py index 02d428c04..3f4bf13e5 100644 --- a/src/core-skills/bmad-brainstorming/scripts/brain.py +++ b/src/core-skills/bmad-brainstorming/scripts/brain.py @@ -433,58 +433,88 @@ SELECTOR_TEMPLATE = r""" BMad Method Brainstorming Selection +
-

BMad Method Brainstorming Selection

+
+
+

BMad Method Brainstorming Selection

+ +

Compose your session, hit Copy prompt, and paste it back into the chat to begin. {{TOTAL}}

@@ -495,6 +525,7 @@ SELECTOR_TEMPLATE = r"""
+
Techniques @@ -503,16 +534,17 @@ SELECTOR_TEMPLATE = r""" Invent 0 AI picks 0 Total 0 · 3–4 is the sweet spot +
- - + Jump to +
{{CHIPS}}
-
{{CHIPS}}
+
{{BODY}} @@ -523,15 +555,31 @@ SELECTOR_TEMPLATE = r""" var $ = function(id){ return document.getElementById(id); }; var all = Array.prototype.slice; var boxes = all.call(document.querySelectorAll('input[type=checkbox]')); - var q = $('q'); + var header = document.querySelector('header'); + var sections = all.call(document.querySelectorAll('section')); var state = { mode: 'Facilitator', rand: 0, inv: 0, ai: 0 }; - var offCats = {}; + var MODE_HINTS = { + 'Facilitator': 'A forcing function for your ideas — I prompt and push, but never supply them.', + 'Creative Partner': 'We riff together — I facilitate and add ideas too, each logged as yours or mine.', + 'Ideate for me': 'I run the whole session myself, then show you the result and offer to keep going.' + }; + function setHint(){ $('modehint').textContent = MODE_HINTS[state.mode] || ''; } + + var themeBtn = $('theme'); + function setThemeIcon(){ themeBtn.textContent = document.documentElement.getAttribute('data-theme') === 'dark' ? '☀' : '☾'; } + themeBtn.addEventListener('click', function(){ + var next = document.documentElement.getAttribute('data-theme') === 'dark' ? 'light' : 'dark'; + document.documentElement.setAttribute('data-theme', next); + try { localStorage.setItem('bmad-theme', next); } catch(e){} + setThemeIcon(); + }); all.call(document.querySelectorAll('.mode')).forEach(function(b){ b.addEventListener('click', function(){ all.call(document.querySelectorAll('.mode')).forEach(function(m){ m.classList.remove('on'); }); b.classList.add('on'); state.mode = b.dataset.mode; + setHint(); }); }); @@ -543,25 +591,21 @@ SELECTOR_TEMPLATE = r""" }); }); + // Category chips are jump-nav: click one to smooth-scroll its section into view, + // offsetting by the sticky header's height so the heading isn't hidden beneath it. all.call(document.querySelectorAll('.chip')).forEach(function(chip){ chip.addEventListener('click', function(){ - var on = !chip.classList.contains('on'); - chip.classList.toggle('on', on); - if (on){ delete offCats[chip.dataset.cat]; } else { offCats[chip.dataset.cat] = true; } - applyFilter(); - update(); // a toggled-off category leaves the session, so counts must refresh too + var sec = null; + for (var i = 0; i < sections.length; i++){ if (sections[i].dataset.cat === chip.dataset.cat){ sec = sections[i]; break; } } + if (!sec){ return; } + var top = sec.getBoundingClientRect().top + window.pageYOffset - header.offsetHeight - 8; + window.scrollTo({ top: top, behavior: 'smooth' }); }); }); boxes.forEach(function(b){ b.addEventListener('change', update); }); - q.addEventListener('input', applyFilter); - // A category toggled off (offCats) leaves the session entirely; the text filter is a - // transient browse aid that never changes what's selected. So both manual picks and the - // random pool key off offCats — never the search box — keeping the copied prompt in step - // with what the user sees, and never starving a random draw because of a stray filter term. - function inScope(b){ return !offCats[b.dataset.cat]; } - function checked(){ return boxes.filter(function(b){ return b.checked && inScope(b); }); } + function checked(){ return boxes.filter(function(b){ return b.checked; }); } function update(){ $('pickN').textContent = checked().length; @@ -574,20 +618,7 @@ SELECTOR_TEMPLATE = r""" t.classList.toggle('warn', total > 5); } - function applyFilter(){ - var s = q.value.trim().toLowerCase(); - all.call(document.querySelectorAll('label.tech')).forEach(function(l){ - var cat = l.querySelector('input').dataset.cat; - var hay = (l.textContent + ' ' + cat).toLowerCase(); - l.style.display = (!offCats[cat] && (!s || hay.indexOf(s) > -1)) ? '' : 'none'; - }); - all.call(document.querySelectorAll('section')).forEach(function(sec){ - var any = all.call(sec.querySelectorAll('label.tech')).some(function(l){ return l.style.display !== 'none'; }); - sec.style.display = (!offCats[sec.dataset.cat] && any) ? '' : 'none'; - }); - } - - function randomPool(){ return boxes.filter(function(b){ return !b.checked && inScope(b); }); } + function randomPool(){ return boxes.filter(function(b){ return !b.checked; }); } function sample(arr, n){ var a = arr.slice(), out = []; @@ -651,6 +682,8 @@ SELECTOR_TEMPLATE = r""" } else { flash(fallbackCopy(text), text); } }); + setHint(); + setThemeIcon(); update(); })(); @@ -690,7 +723,7 @@ def html_doc(rows: list[dict]) -> str: f'{cat_icon}{t_icon}' f'{name}{desc}' ) - chips.append(f'') + chips.append(f'') sections.append( f'

{disp}{len(groups[cat])}

' f'
{"".join(cards)}
'