bmad-method 6.8.1-next.9 → 6.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. package/.claude-plugin/marketplace.json +9 -3
  2. package/package.json +10 -4
  3. package/src/bmm-skills/1-analysis/bmad-prfaq/SKILL.md +2 -2
  4. package/src/bmm-skills/1-analysis/bmad-product-brief/SKILL.md +8 -8
  5. package/src/bmm-skills/2-plan-workflows/bmad-prd/SKILL.md +7 -7
  6. package/src/bmm-skills/2-plan-workflows/bmad-prd/assets/headless-schemas.md +2 -2
  7. package/src/bmm-skills/2-plan-workflows/bmad-prd/customize.toml +1 -1
  8. package/src/bmm-skills/2-plan-workflows/bmad-prd/references/headless.md +1 -1
  9. package/src/bmm-skills/2-plan-workflows/bmad-prd/references/validate.md +1 -1
  10. package/src/bmm-skills/2-plan-workflows/bmad-ux/SKILL.md +8 -8
  11. package/src/bmm-skills/2-plan-workflows/bmad-ux/assets/design-directions.md +1 -1
  12. package/src/bmm-skills/2-plan-workflows/bmad-ux/assets/headless-schemas.md +2 -2
  13. package/src/bmm-skills/2-plan-workflows/bmad-ux/assets/key-screens.md +4 -4
  14. package/src/bmm-skills/2-plan-workflows/bmad-ux/customize.toml +1 -1
  15. package/src/bmm-skills/2-plan-workflows/bmad-ux/references/creative-tools.md +1 -1
  16. package/src/bmm-skills/2-plan-workflows/bmad-ux/references/headless.md +1 -1
  17. package/src/bmm-skills/2-plan-workflows/bmad-ux/references/validate.md +2 -2
  18. package/src/bmm-skills/3-solutioning/bmad-agent-architect/customize.toml +2 -2
  19. package/src/bmm-skills/3-solutioning/bmad-architecture/SKILL.md +85 -0
  20. package/src/bmm-skills/3-solutioning/bmad-architecture/assets/spine-template.md +79 -0
  21. package/src/bmm-skills/3-solutioning/bmad-architecture/customize.toml +100 -0
  22. package/src/bmm-skills/3-solutioning/bmad-architecture/references/headless.md +26 -0
  23. package/src/bmm-skills/3-solutioning/bmad-architecture/references/reviewer-gate.md +13 -0
  24. package/src/bmm-skills/3-solutioning/bmad-architecture/scripts/lint_spine.py +257 -0
  25. package/src/bmm-skills/3-solutioning/bmad-architecture/scripts/tests/test_lint_spine.py +270 -0
  26. package/src/bmm-skills/3-solutioning/bmad-create-architecture/SKILL.md +16 -60
  27. package/src/bmm-skills/4-implementation/bmad-retrospective/SKILL.md +15 -0
  28. package/src/bmm-skills/4-implementation/bmad-sprint-planning/SKILL.md +20 -1
  29. package/src/bmm-skills/4-implementation/bmad-sprint-planning/checklist.md +2 -1
  30. package/src/bmm-skills/4-implementation/bmad-sprint-planning/sprint-status-template.yaml +13 -0
  31. package/src/bmm-skills/4-implementation/bmad-sprint-status/SKILL.md +13 -0
  32. package/src/bmm-skills/module-help.csv +2 -2
  33. package/src/core-skills/bmad-brainstorming/SKILL.md +8 -10
  34. package/src/core-skills/bmad-brainstorming/references/converge.md +1 -1
  35. package/src/core-skills/bmad-brainstorming/references/finalize.md +1 -1
  36. package/src/core-skills/bmad-brainstorming/references/headless.md +4 -4
  37. package/src/core-skills/bmad-brainstorming/references/in-chat-techniques.md +1 -1
  38. package/src/core-skills/bmad-brainstorming/references/mode-autonomous.md +1 -1
  39. package/src/core-skills/bmad-brainstorming/scripts/tests/test_brain.py +2 -2
  40. package/src/core-skills/bmad-customize/scripts/tests/test_list_customizable_skills.py +1 -1
  41. package/src/core-skills/bmad-forge-idea/SKILL.md +79 -0
  42. package/src/core-skills/bmad-forge-idea/customize.toml +42 -0
  43. package/src/core-skills/bmad-forge-idea/scripts/resolve_personas.py +270 -0
  44. package/src/core-skills/bmad-forge-idea/scripts/tests/test_resolve_personas.py +138 -0
  45. package/src/core-skills/bmad-party-mode/SKILL.md +39 -56
  46. package/src/core-skills/bmad-party-mode/customize.toml +175 -0
  47. package/src/core-skills/bmad-party-mode/references/create-party.md +70 -0
  48. package/src/core-skills/bmad-party-mode/references/mode-agent-team.md +11 -0
  49. package/src/core-skills/bmad-party-mode/references/mode-auto.md +13 -0
  50. package/src/core-skills/bmad-party-mode/references/mode-subagent.md +19 -0
  51. package/src/core-skills/bmad-party-mode/references/party-memory.md +51 -0
  52. package/src/core-skills/bmad-party-mode/scripts/resolve_party.py +272 -0
  53. package/src/core-skills/bmad-party-mode/scripts/tests/test-resolve_party.py +146 -0
  54. package/src/core-skills/bmad-spec/SKILL.md +5 -3
  55. package/src/core-skills/bmad-spec/assets/spec-template.md +3 -3
  56. package/src/core-skills/module-help.csv +1 -0
  57. package/src/scripts/resolve_config.py +8 -6
  58. package/src/scripts/resolve_customization.py +8 -6
  59. package/tools/installer/commands/install.js +3 -0
  60. package/tools/installer/core/installer.js +3 -0
  61. package/tools/installer/core/uv-check.js +97 -0
  62. package/tools/installer/core/wsl-node-check.js +109 -0
  63. package/tools/installer/ide/platform-codes.yaml +7 -0
  64. package/tools/installer/install-messages.yaml +4 -0
  65. package/tools/installer/ui.js +10 -9
  66. package/evals/bmm-skills/bmad-product-brief/evals.json +0 -237
  67. package/evals/bmm-skills/bmad-product-brief/files/branfield-memo.md +0 -46
  68. package/evals/bmm-skills/bmad-product-brief/files/forkbird-brief/addendum.md +0 -40
  69. package/evals/bmm-skills/bmad-product-brief/files/forkbird-brief/brief.md +0 -56
  70. package/evals/bmm-skills/bmad-product-brief/files/forkbird-brief/decision-log.md +0 -27
  71. package/evals/bmm-skills/bmad-product-brief/files/meridian-mobility-report.md +0 -116
  72. package/evals/bmm-skills/bmad-product-brief/files/mossridge-brief/addendum.md +0 -41
  73. package/evals/bmm-skills/bmad-product-brief/files/mossridge-brief/brief.md +0 -57
  74. package/evals/bmm-skills/bmad-product-brief/files/mossridge-brief/decision-log.md +0 -29
  75. package/evals/bmm-skills/bmad-product-brief/files/pantry-bridge-interviews.md +0 -90
  76. package/evals/bmm-skills/bmad-product-brief/files/q2-brainstorm.md +0 -101
  77. package/evals/bmm-skills/bmad-product-brief/triggers.json +0 -18
  78. package/src/bmm-skills/3-solutioning/bmad-create-architecture/architecture-decision-template.md +0 -12
  79. package/src/bmm-skills/3-solutioning/bmad-create-architecture/data/domain-complexity.csv +0 -13
  80. package/src/bmm-skills/3-solutioning/bmad-create-architecture/data/project-types.csv +0 -7
  81. package/src/bmm-skills/3-solutioning/bmad-create-architecture/steps/step-01-init.md +0 -153
  82. package/src/bmm-skills/3-solutioning/bmad-create-architecture/steps/step-01b-continue.md +0 -173
  83. package/src/bmm-skills/3-solutioning/bmad-create-architecture/steps/step-02-context.md +0 -224
  84. package/src/bmm-skills/3-solutioning/bmad-create-architecture/steps/step-03-starter.md +0 -329
  85. package/src/bmm-skills/3-solutioning/bmad-create-architecture/steps/step-04-decisions.md +0 -318
  86. package/src/bmm-skills/3-solutioning/bmad-create-architecture/steps/step-05-patterns.md +0 -359
  87. package/src/bmm-skills/3-solutioning/bmad-create-architecture/steps/step-06-structure.md +0 -379
  88. package/src/bmm-skills/3-solutioning/bmad-create-architecture/steps/step-07-validation.md +0 -361
  89. package/src/bmm-skills/3-solutioning/bmad-create-architecture/steps/step-08-complete.md +0 -82
  90. package/src/core-skills/bmad-brainstorming/scripts/memlog.py +0 -202
  91. package/src/core-skills/bmad-brainstorming/scripts/tests/test_memlog.py +0 -265
  92. package/tools/installer/core/python-check.js +0 -199
@@ -14,7 +14,7 @@ When in doubt, you are interactive — a present human asking you to "brainstorm
14
14
 
15
15
  ## The inversion
16
16
 
17
- There is no user to draw ideas out of, so you become the brainstormer. Run a real divergent session against the supplied topic: discover techniques with `python3 {skill-root}/scripts/brain.py --file {workflow.brain_methods} list --all` (the whole catalog is fine here — you are generating, not pacing a user; add `show "<name>"` for a technique's full method on demand), plus any `{workflow.additional_techniques}`, preferring `{workflow.favorite_techniques}` where they fit; work them, and **shift the creative domain every ~10 ideas** exactly as the interactive Stance demands — technical, then experiential, then business, then failure modes, then wildcards. Push past the obvious; the same quantity ambition (aim past 100) and anti-clustering discipline apply. The only thing that changes is that the ideas are now yours to generate. This relaxation is scoped entirely to this file — it never applies to interactive sessions.
17
+ There is no user to draw ideas out of, so you become the brainstormer. Run a real divergent session against the supplied topic: discover techniques with `uv run {skill-root}/scripts/brain.py --file {workflow.brain_methods} list --all` (the whole catalog is fine here — you are generating, not pacing a user; add `show "<name>"` for a technique's full method on demand), plus any `{workflow.additional_techniques}`, preferring `{workflow.favorite_techniques}` where they fit; work them, and **shift the creative domain every ~10 ideas** exactly as the interactive Stance demands — technical, then experiential, then business, then failure modes, then wildcards. Push past the obvious; the same quantity ambition (aim past 100) and anti-clustering discipline apply. The only thing that changes is that the ideas are now yours to generate. This relaxation is scoped entirely to this file — it never applies to interactive sessions.
18
18
 
19
19
  ## Inputs the caller is expected to provide
20
20
 
@@ -29,9 +29,9 @@ Free-form structured payload in the first message; provide what applies:
29
29
 
30
30
  ## Run
31
31
 
32
- 1. Bind `{doc_workspace}` and create the memlog with `python3 {skill-root}/scripts/memlog.py init --workspace {doc_workspace} --field topic="<topic>" [--field goal="<goal>"]`. It remains the canonical source every artifact derives from.
33
- 2. Run the divergent session per **The inversion**, capturing each idea with `memlog.py append --workspace {doc_workspace} --type idea --text "<idea>"` as it lands, and marking each technique switch with `memlog.py append --type technique --text "started <name>"`.
34
- 3. Synthesize: surface the conclusions, connections, and the few directions that matter; record them with `memlog.py append --type insight`, then run `memlog.py set --workspace {doc_workspace} --key status --value complete`.
32
+ 1. Bind `{doc_workspace}` and create the memlog with `uv run {project-root}/_bmad/scripts/memlog.py init --workspace {doc_workspace} --field topic="<topic>" [--field goal="<goal>"]`. It remains the canonical source every artifact derives from.
33
+ 2. Run the divergent session per **The inversion**, capturing each idea with `uv run {project-root}/_bmad/scripts/memlog.py append --workspace {doc_workspace} --type idea --text "<idea>"` as it lands, and marking each technique switch with `uv run {project-root}/_bmad/scripts/memlog.py append --workspace {doc_workspace} --type technique --text "started <name>"`.
34
+ 3. Synthesize: surface the conclusions, connections, and the few directions that matter; record them with `uv run {project-root}/_bmad/scripts/memlog.py append --workspace {doc_workspace} --type insight --text "<insights>"`, then run `uv run {project-root}/_bmad/scripts/memlog.py set --workspace {doc_workspace} --key status --value complete`.
35
35
  4. Produce the requested artifacts from the log — `brainstorm.html` (the imaginative, self-contained, no-template report) and/or the succinct `brainstorm-intent.md` — the same artifacts `references/finalize.md` describes, delegating each to a subagent that reads the log as its sole source. (Headless produces the `artifacts` payload directly; it does not ask, unlike the interactive opt-in.)
36
36
  5. Execute each entry in `{workflow.external_handoffs}` (capture returned URLs/IDs into the JSON `external_handoffs` array; skip and flag unavailable tools — local files always exist). Then run `{workflow.on_complete}` if non-empty.
37
37
 
@@ -7,7 +7,7 @@ Loaded only when the user won't use the composer page (no browser, headless, or
7
7
  - **Category** — the user names 1–n categories; `random --category` draws the batch from them. No listing needed.
8
8
  - **Inventive Flow** — invent at least 3 techniques, announce the order before the first, touch no script. Log each one's name + description so you can offer to save a keeper to `{workflow.additional_techniques}` (via `bmad-customize`) at wrap-up.
9
9
 
10
- The library is large — never pull it whole into context. The only way in is the helper, always passing `--file {workflow.brain_methods}`. Subcommands of `python3 {skill-root}/scripts/brain.py --file {workflow.brain_methods}`:
10
+ The library is large — never pull it whole into context. The only way in is the helper, always passing `--file {workflow.brain_methods}`. Subcommands of `uv run {skill-root}/scripts/brain.py --file {workflow.brain_methods}`:
11
11
 
12
12
  - `categories` — names + counts; the cheap survey map.
13
13
  - `list --category X [--category Y]` — the index (name + gist) for those categories. Bare `list` is refused by the script.
@@ -5,6 +5,6 @@ The user handed you the topic and wants to see what you come up with on your own
5
5
  - **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.
6
6
  - **Don't pepper the user with questions** — this is your run. One quick confirm of topic and goal up front is plenty.
7
7
  - **When it's mined out, synthesize and produce the keepsake.** Go to `## Wrap-Up` (`references/finalize.md`): record the insights, mark the memlog complete, and **auto-generate the imaginative HTML keepsake — don't ask first; the keepsake is the result you promised to show them.** Offer the other artifacts (intent doc, etc.) after.
8
- - **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 <facilitator|partner>` — and continue from the same memlog.
8
+ - **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 — `uv run {project-root}/_bmad/scripts/memlog.py set --workspace {doc_workspace} --key mode --value <facilitator|partner>` — and continue from the same memlog.
9
9
 
10
10
  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.
@@ -207,11 +207,11 @@ def test_unknown_category_style_uses_fallback_glyph():
207
207
 
208
208
  def test_shipped_selector_is_in_sync_with_catalog():
209
209
  # foolproofing: if someone edits brain-methods.csv they must regenerate the page.
210
- # Regenerate with: python3 brain.py html --out assets/brain-selector.html
210
+ # Regenerate with: uv run brain.py html --out assets/brain-selector.html
211
211
  asset = brain.DEFAULT_FILE.parent / "brain-selector.html"
212
212
  assert asset.is_file(), "missing assets/brain-selector.html — generate it"
213
213
  expected = brain.html_doc(brain.load(brain.DEFAULT_FILE))
214
214
  assert asset.read_text(encoding="utf-8") == expected, (
215
215
  "assets/brain-selector.html is stale; regenerate: "
216
- "python3 brain.py html --out assets/brain-selector.html"
216
+ "uv run brain.py html --out assets/brain-selector.html"
217
217
  )
@@ -13,7 +13,7 @@ Exercises the scanner against a synthesized install tree:
13
13
  - malformed TOML (surfaces as an error without aborting)
14
14
  - multiple skills roots (e.g. project-local + user-global mix)
15
15
 
16
- Run: python3 scripts/tests/test_list_customizable_skills.py
16
+ Run: uv run scripts/tests/test_list_customizable_skills.py
17
17
  """
18
18
 
19
19
  from __future__ import annotations
@@ -0,0 +1,79 @@
1
+ ---
2
+ name: bmad-forge-idea
3
+ description: Pressure-test an idea through persona-driven interrogation until it hardens, proves out, or dies cheaply. Use when the user says 'forge an idea', 'pressure-test this idea', 'stress-test my thinking', or 'harden this idea'.
4
+ ---
5
+
6
+ # BMad Forge Idea
7
+
8
+ ## Overview
9
+
10
+ Take a half-formed idea out of the user's head and pressure-test it now, in conversation, where changing your mind is free — until what survives is something they can act on with earned conviction, or it dies cheaply. The enemy is the hole you cannot see in your own idea: every unexamined assumption and unresolved branch is a crack that otherwise surfaces later, in the build or the launch, when it costs far more to fix.
11
+
12
+ The product is the quality of the user's thinking, not an artifact. Hardening an idea, proving or disproving it, or just being an unsparing thinking partner are each a complete outcome. A distilled `forged-idea.md` and a handoff downstream are one optional exit, never the destination — so never herd the user toward "shall we build it?"
13
+
14
+ This is domain-agnostic — the idea may be software, a business model, a creative concept, a research hypothesis, a life decision, or a frivolous thought experiment. When it's a product or feature — net-new or a change inside an existing project — the forge stands in as an alternative analysis-and-definition tool, and what survives distills into `forged-idea.md` for downstream planning.
15
+
16
+ Act as an exacting interrogator who would rather find the crack than spare the feelings. This is interactive and socratic by nature; there is no headless mode.
17
+
18
+ ## Conventions
19
+
20
+ - Scripts live in two places — run each from the exact path written, never assume co-location: the shared core scripts (`memlog.py`, `resolve_customization.py`, `resolve_config.py`) are installed by BMad core at `{project-root}/_bmad/scripts/` and are never bundled here; this skill's own `resolve_personas.py` is at `{skill-root}/scripts/`.
21
+ - `{workflow.<name>}` resolves to fields in the merged `customize.toml` `[workflow]` table.
22
+
23
+ ## On Activation
24
+
25
+ 1. Resolve customization: `uv run {project-root}/_bmad/scripts/resolve_customization.py --skill {skill-root} --key workflow`. On failure, read `{skill-root}/customize.toml` directly with defaults. Apply the resolved `{workflow.*}` values throughout.
26
+ 2. Run each `{workflow.activation_steps_prepend}` entry; treat each `{workflow.persistent_facts}` entry as foundational context (`file:` entries load their contents, `skill:` names a skill to consult, others are facts verbatim).
27
+ 3. Load `{project-root}/_bmad/core/config.yaml` (and `config.user.yaml` if present); resolve `{user_name}`, `{communication_language}`, `{output_folder}`. Missing → neutral defaults; never block. Greet `{user_name}` in `{communication_language}` and stay in it.
28
+ 4. Note whether a BMad persona is already active in this conversation — the user loaded one (e.g. the analyst, the storyteller) and invoked the forge from within it. If so, that persona leads the session, in voice, throughout.
29
+ 5. Resume: glob `{workflow.forge_output_path}/**/.memlog.md` (recursive, so it still finds sessions when `run_folder_pattern` is overridden to nest paths) and read only each match's frontmatter to find any whose `status` is not `complete`. Offer to resume one — then read its full memlog once to rebuild state and continue append-only — or to start fresh.
30
+ 6. Run each `{workflow.activation_steps_append}` entry.
31
+
32
+ ## Open the session
33
+
34
+ Open cold. Acknowledging the idea is not endorsing it — do not praise it before it has survived anything, on this turn or any turn. The pull to validate the idea up front to build rapport is the exact reflex this skill exists to refuse.
35
+
36
+ Determine the goal before pressing (if a persona is already active with an idea on the table, confirm it in a line rather than asking). Otherwise ask in one message: what is the idea, and what do you want — harden it, prove or kill it, or just think it through? The goal steers the push: proving goes for the load-bearing claim first; hardening drives each branch to a resolved answer. Note whether the idea is net-new or a change inside an existing project.
37
+
38
+ Tell the user the gear they can call anytime: **"adversarial on this"** (attacked to destruction — you attack, they defend; "switch roles," "you defend now, they attack"). The room is always in play once the topic is set (see The personas) — they can name any persona or call a whole party by name to steer who's at the table.
39
+
40
+ Derive a kebab-case `{slug}` for the idea and bind the session workspace `{workspace} = {workflow.forge_output_path}/{workflow.run_folder_pattern}` (the pattern fills with `{slug}`). Create the memlog once the goal is known:
41
+ `uv run {project-root}/_bmad/scripts/memlog.py init --workspace {workspace} --field idea="<idea>" --field goal="<goal>"`
42
+ Tell the user the path; state is on disk now, so the session survives interruption. If init fails, don't abort — run the forge in-conversation and tell the user state won't persist this session.
43
+
44
+ ## The forge
45
+
46
+ Work one question at a time, in dependency order. Put your own recommended answer on the table each time — a position to push against gets further than an open prompt. Find discoverable answers yourself rather than asking. Treat the user's own words as suspect too: when a term is fuzzy or carries two meanings — a business 'user' versus 'buyer' versus 'payer', not just a code noun — name the ambiguity and force a precise choice before the branch resolves, because a branch built on an overloaded word resolves falsely. When the idea lands inside an existing project, that project's material is your ground truth, and a label is not a move: find the relevant material yourself, check the claim against it, and when it contradicts, make the contradiction the next question. When a branch resolves, give the user a beat before moving on — the crack they were holding back surfaces in that opening.
47
+
48
+ **Never default-agree.** Reflexive agreement lowers the pressure and the user thinks shallower for it. Attack the weak point or build on the strong one — whichever drives deeper thinking — and praise only what genuinely earns it. The objective is the best idea, not a comfortable user.
49
+
50
+ Capture as you go — each decision, assumption, crack, kill, and locked idea, one bullet in the user's meaning:
51
+ `uv run {project-root}/_bmad/scripts/memlog.py append --workspace {workspace} --type <decision|assumption|crack|kill|direction|lock|note> --text "<gist>"`
52
+ A `lock` is an idea the user hardens — settled, not to be reopened; locks are what `forged-idea.md` is distilled from. Don't read the memlog back except on resume. If the user raises a different branch, capture it and stay put — the loop and the stray insight both survive.
53
+
54
+ ## The personas
55
+
56
+ The forge is voiced, not generic — and once the topic is set it always runs with the room, because a branch worked by two sharp characters goes deeper and lands harder than a faceless assistant ever could. A persona loaded at activation leads throughout and holds character.
57
+
58
+ Resolve the pool once, as soon as the goal is known:
59
+ `uv run {skill-root}/scripts/resolve_personas.py --project-root {project-root} --skill {skill-root}`
60
+ It returns the installed BMad roster (`agents`), any custom personas the user authored (`members`), and their saved party groups (`parties` — each with an optional `scene` to play, open-cast rooms flagged) — everything `bmad-party-mode` knows, without invoking it.
61
+
62
+ From then on, every turn brings two voices to the branch — witnesses you cross-examine, not a panel that debates:
63
+ - **One from the user's pool** — an installed agent or custom persona they'll recognize, whose expertise fits the branch in play. Vary who shows up every few turns to keep the pressure high and the angles fresh; don't let the same voice dominate. If the user calls a specific name, bring them in. If the pool resolves empty (a core-only install with no roster), generate both voices on the fly so every branch still arrives with two.
64
+ - **One you generate on the fly** — a fresh persona the topic conjures (a hostile competitor, a skeptical CFO, a domain specialist, a historical persona or expert), named and characterized so it's unmistakably itself.
65
+
66
+ They hammer the branch in character; you synthesize their hits into your next question and drive it to a resolved answer. The user steers anytime — name a specific person, call a whole saved party for its scene, or go one-on-one. Voice them yourself by default; spawn separate agents (as `bmad-party-mode` does) only when a branch needs genuinely independent minds — a verdict that shouldn't be colored by one voice speaking for all.
67
+
68
+ ## Exits
69
+
70
+ The session ends however the thinking lands, and every landing is a real outcome:
71
+
72
+ - **Hardened** — the idea survived. Distill the memlog into `{workspace}/forged-idea.md`: super succinct — the locked items and what was killed and why, in the user's meaning. Not a prose retelling, not a template, not the conversation replayed — the load-bearing residue, nothing else. If it reads like a document, it's too long. Note it can feed `bmad-spec`, `bmad-prd`, or `bmad-prfaq`.
73
+ - **Killed** — the idea did not survive. Say so plainly and record why. Finding this cheaply is a win, not a failure.
74
+ - **Clearer** — the user simply thinks straighter now. The memlog stands on its own; no `forged-idea.md` needed (the report below still renders).
75
+
76
+ However it lands, render the verdict as a self-contained HTML report the user can open — `{workspace}/forge-report.html`, written every time, no asking. Strike it with a bespoke wax-seal/stamp matched to the outcome: **HARDENED** for a survivor, an **Idea Death Certificate** stamped **KILLED** (with the cause of death) for one that didn't, or a fitting bespoke seal for wherever else it landed (e.g. **CLARIFIED**). Lay out the load-bearing residue — the locked items, what was killed and why, the cracks that held — in the user's meaning, and credit the room: the personas and parties that pressure-tested it, by name, icon, and voice. One nicely-styled page (inline CSS, an inline-SVG seal, light flourish only where it lifts the piece) — a genuine keepsake, not a templated dump. Tell the user the path.
77
+
78
+ Flip the status at the end: `uv run {project-root}/_bmad/scripts/memlog.py set --workspace {workspace} --key status --value complete`.
79
+ If `{workflow.on_complete}` is non-empty, run all instructions in order.
@@ -0,0 +1,42 @@
1
+ # DO NOT EDIT -- overwritten on every update.
2
+ #
3
+ # Workflow customization surface for bmad-forge-idea.
4
+ #
5
+ # Override files (not edited here):
6
+ # {project-root}/_bmad/custom/bmad-forge-idea.toml (team)
7
+ # {project-root}/_bmad/custom/bmad-forge-idea.user.toml (personal)
8
+
9
+ [workflow]
10
+
11
+ # --- Configurable below. Overrides merge per BMad structural rules: ---
12
+ # scalars: override wins • arrays: append
13
+
14
+ # Steps to run before the standard activation (config load, greet).
15
+ activation_steps_prepend = []
16
+
17
+ # Steps to run after greet but before the session begins.
18
+ activation_steps_append = []
19
+
20
+ # Persistent facts the interrogator keeps in mind for the whole session
21
+ # (domain constraints, house rules, what's off the table). Each entry is a
22
+ # literal sentence, a skill prefixed with `skill:`, or a `file:`-prefixed
23
+ # path/glob whose contents are loaded as facts. Default loads project-context.md
24
+ # when one exists (e.g. from bmad-generate-project-context), so the forge grounds
25
+ # in the project's tech, domain, and constraints without re-asking.
26
+ persistent_facts = [
27
+ "file:{project-root}/**/project-context.md",
28
+ ]
29
+
30
+ # Executed when the session completes. Scalar or array of instructions. Empty for none.
31
+ on_complete = []
32
+
33
+ # Parent folder for all forge sessions. Each session gets its own run
34
+ # folder underneath (see run_folder_pattern). Lands directly under
35
+ # {output_folder} so the forge works in core-only installs.
36
+ forge_output_path = "{output_folder}/forge"
37
+
38
+ # Run-folder pattern inside forge_output_path. Resolved against the
39
+ # idea-derived slug at activation. Same slug = same folder, so resuming
40
+ # an idea reuses its memlog. Override to add {date} or other components
41
+ # if a fresh dated history per run is preferred.
42
+ run_folder_pattern = "{slug}"
@@ -0,0 +1,270 @@
1
+ #!/usr/bin/env python3
2
+ # /// script
3
+ # requires-python = ">=3.11"
4
+ # ///
5
+ """Resolve the personas and parties the forge can bring into the room.
6
+
7
+ The forge cross-examines witnesses: the installed BMAD agents, plus any
8
+ custom personas and party groups the user has authored for `bmad-party-mode`.
9
+ This surfaces all of them in one shot so the orchestrator never has to ask
10
+ "who's available?" — it just intermixes whoever fits the branch, alongside
11
+ any persona the user names on the fly.
12
+
13
+ What it returns (JSON, stdout):
14
+ * agents — the installed BMAD roster: the default room, always present.
15
+ * members — extra custom personas in the pool (party_members the user
16
+ defined that aren't already an installed slot).
17
+ * parties — the user's named party groups, members resolved to brief
18
+ entries; open-cast groups (scene names a pool, no roster)
19
+ are flagged.
20
+ * default_party — the group id pinned as party-mode's default, if any.
21
+
22
+ Discovery is best-effort and never blocks the forge. The installed roster
23
+ comes from the core resolver; custom personas/parties come from
24
+ `bmad-party-mode`'s resolved customization when that skill is found beside
25
+ this one, else from the user's override TOMLs read directly. Anything that
26
+ can't be resolved is simply omitted and flagged, never fatal.
27
+
28
+ Stdlib only (Python 3.11+ for tomllib).
29
+
30
+ resolve_personas.py --project-root P --skill S
31
+ """
32
+
33
+ import argparse
34
+ import json
35
+ import subprocess
36
+ import sys
37
+ from pathlib import Path
38
+
39
+ try:
40
+ import tomllib
41
+ except ImportError: # pragma: no cover - guarded for <3.11
42
+ sys.stderr.write("error: Python 3.11+ is required (stdlib `tomllib`).\n")
43
+ sys.exit(3)
44
+
45
+ PARTY_SKILL = "bmad-party-mode"
46
+
47
+
48
+ def _run_json(cmd):
49
+ """Run a resolver script and parse its JSON stdout. None on any failure."""
50
+ try:
51
+ out = subprocess.run(cmd, capture_output=True, text=True, timeout=60)
52
+ except (OSError, subprocess.SubprocessError):
53
+ return None
54
+ if out.returncode != 0 or not out.stdout.strip():
55
+ return None
56
+ try:
57
+ return json.loads(out.stdout)
58
+ except json.JSONDecodeError:
59
+ return None
60
+
61
+
62
+ def _load_toml(path: Path):
63
+ if not path.exists():
64
+ return {}
65
+ try:
66
+ with path.open("rb") as f:
67
+ data = tomllib.load(f)
68
+ return data if isinstance(data, dict) else {}
69
+ except (OSError, tomllib.TOMLDecodeError):
70
+ return {}
71
+
72
+
73
+ def load_agents(project_root: Path):
74
+ """Installed BMAD agents as {code: entry}. (dict, resolved_ok).
75
+
76
+ The core resolver may emit agents as a dict keyed by code or as an array
77
+ of tables (depending on how the layers merged); normalize both to a dict.
78
+ """
79
+ script = project_root / "_bmad" / "scripts" / "resolve_config.py"
80
+ data = _run_json([sys.executable, str(script), "--project-root", str(project_root), "--key", "agents"])
81
+ if data is None:
82
+ return {}, False
83
+ agents = data.get("agents", {}) or {}
84
+ if isinstance(agents, list):
85
+ agents = {a["code"]: a for a in agents if isinstance(a, dict) and a.get("code")}
86
+ elif not isinstance(agents, dict):
87
+ agents = {}
88
+ return agents, True
89
+
90
+
91
+ def find_party_skill(project_root: Path, skill_root: Path):
92
+ """Locate the installed bmad-party-mode skill dir, or None.
93
+
94
+ Skills install as siblings, so the party skill is almost always next to
95
+ this one. A couple of common install roots cover the rest.
96
+ """
97
+ candidates = [
98
+ skill_root.parent / PARTY_SKILL,
99
+ project_root / ".claude" / "skills" / PARTY_SKILL,
100
+ project_root / "_bmad" / "skills" / PARTY_SKILL,
101
+ ]
102
+ for c in candidates:
103
+ if (c / "customize.toml").exists():
104
+ return c
105
+ return None
106
+
107
+
108
+ def load_party_workflow(project_root: Path, party_skill: Path):
109
+ """Merged [workflow] table for bmad-party-mode (base + user overrides)."""
110
+ resolver = project_root / "_bmad" / "scripts" / "resolve_customization.py"
111
+ data = _run_json([sys.executable, str(resolver), "--skill", str(party_skill), "--key", "workflow"])
112
+ if data is not None and isinstance(data.get("workflow"), dict):
113
+ return data["workflow"]
114
+ # Fallback: base customize.toml directly, no override merge.
115
+ wf = _load_toml(party_skill / "customize.toml").get("workflow", {})
116
+ return wf if isinstance(wf, dict) else {}
117
+
118
+
119
+ def load_party_overrides(project_root: Path):
120
+ """Custom personas/parties when party-mode itself isn't installed.
121
+
122
+ Reads only the user's override TOMLs (team then personal, personal wins on
123
+ scalars). No base roster exists in this path, so a shallow merge is enough.
124
+ """
125
+ custom = project_root / "_bmad" / "custom"
126
+ team = _load_toml(custom / f"{PARTY_SKILL}.toml").get("workflow", {})
127
+ user = _load_toml(custom / f"{PARTY_SKILL}.user.toml").get("workflow", {})
128
+ team = team if isinstance(team, dict) else {}
129
+ user = user if isinstance(user, dict) else {}
130
+ merged = dict(team)
131
+ for key, val in user.items():
132
+ if isinstance(val, list) and isinstance(merged.get(key), list):
133
+ merged[key] = merged[key] + val
134
+ else:
135
+ merged[key] = val
136
+ return merged
137
+
138
+
139
+ def _alias(code: str) -> str:
140
+ """Short alias for an installed agent code: bmad-agent-analyst -> analyst."""
141
+ for prefix in ("bmad-agent-", "bmad-"):
142
+ if code.startswith(prefix):
143
+ return code[len(prefix):]
144
+ return code
145
+
146
+
147
+ def build_pool(agents: dict, party_members: list):
148
+ """One pool keyed by code; custom members override matching installed slots.
149
+
150
+ Returns (pool, index, installed_codes, custom_codes):
151
+ * installed_codes — the default room (installed agents, overrides applied
152
+ in place); custom-only additions stay in the pool but don't crowd it.
153
+ * custom_codes — pure-custom personas (no installed slot), the extra
154
+ faces the forge can summon by name or via a party group.
155
+ """
156
+ pool, index, installed_codes, custom_codes = {}, {}, [], []
157
+
158
+ def register(code, entry):
159
+ pool[code] = entry
160
+ index[code] = code
161
+ index[code.lower()] = code
162
+ index[_alias(code).lower()] = code
163
+ name = entry.get("name")
164
+ if name:
165
+ key = name.lower()
166
+ # A custom rename must not hijack another agent's name lookup.
167
+ if index.get(key, code) == code:
168
+ index[key] = code
169
+
170
+ for code, info in (agents or {}).items():
171
+ register(code, {
172
+ "code": code,
173
+ "name": info.get("name", code),
174
+ "icon": info.get("icon", ""),
175
+ "title": info.get("title", ""),
176
+ "description": info.get("description", ""),
177
+ "source": "installed",
178
+ })
179
+ installed_codes.append(code)
180
+
181
+ for m in (party_members if isinstance(party_members, list) else []):
182
+ if not isinstance(m, dict):
183
+ continue
184
+ code = m.get("code")
185
+ if not code:
186
+ continue
187
+ canonical = index.get(code) or index.get(code.lower()) or code
188
+ was_installed = canonical in pool
189
+ entry = {"code": canonical, "source": "custom"}
190
+ for field in ("name", "icon", "title", "persona", "capabilities", "model"):
191
+ if m.get(field) is not None:
192
+ entry[field] = m[field]
193
+ entry.setdefault("name", canonical)
194
+ register(canonical, entry)
195
+ if not was_installed:
196
+ custom_codes.append(canonical)
197
+
198
+ return pool, index, installed_codes, custom_codes
199
+
200
+
201
+ def _brief(entry):
202
+ """The slim card the orchestrator needs to cast a persona."""
203
+ out = {k: entry[k] for k in ("code", "name", "icon", "title", "source") if entry.get(k)}
204
+ for k in ("description", "persona", "capabilities", "model"):
205
+ if entry.get(k):
206
+ out[k] = entry[k]
207
+ return out
208
+
209
+
210
+ def resolve_parties(groups, pool, index):
211
+ out = []
212
+ for g in groups or []:
213
+ if not isinstance(g, dict) or not g.get("id"):
214
+ continue
215
+ raw = g.get("members", []) or []
216
+ members = []
217
+ for t in raw:
218
+ key = t if isinstance(t, str) else str(t)
219
+ code = index.get(key) or index.get(key.lower())
220
+ if code in pool:
221
+ members.append(_brief(pool[code]))
222
+ party = {"id": g["id"], "name": g.get("name", g["id"]), "members": members}
223
+ if g.get("scene"):
224
+ party["scene"] = g["scene"]
225
+ if not raw:
226
+ party["open_cast"] = True
227
+ out.append(party)
228
+ return out
229
+
230
+
231
+ def main():
232
+ ap = argparse.ArgumentParser(description="Resolve forge personas and parties.")
233
+ ap.add_argument("--project-root", required=True)
234
+ ap.add_argument("--skill", required=True, help="Path to the bmad-forge-idea skill dir")
235
+ args = ap.parse_args()
236
+
237
+ project_root = Path(args.project_root).resolve()
238
+ skill_root = Path(args.skill).resolve()
239
+
240
+ agents, agents_ok = load_agents(project_root)
241
+
242
+ party_skill = find_party_skill(project_root, skill_root)
243
+ if party_skill is not None:
244
+ workflow = load_party_workflow(project_root, party_skill)
245
+ else:
246
+ workflow = load_party_overrides(project_root)
247
+
248
+ pool, index, installed_codes, custom_codes = build_pool(
249
+ agents, workflow.get("party_members", []))
250
+ parties = resolve_parties(workflow.get("party_groups", []), pool, index)
251
+
252
+ _emit({
253
+ "agents": [_brief(pool[c]) for c in installed_codes],
254
+ "members": [_brief(pool[c]) for c in custom_codes],
255
+ "parties": parties,
256
+ "default_party": workflow.get("default_party", "") or "",
257
+ "party_mode_found": party_skill is not None,
258
+ "agents_resolved": agents_ok,
259
+ })
260
+
261
+
262
+ def _emit(obj):
263
+ reconfigure = getattr(sys.stdout, "reconfigure", None)
264
+ if reconfigure is not None:
265
+ reconfigure(encoding="utf-8")
266
+ sys.stdout.write(json.dumps(obj, indent=2, ensure_ascii=False) + "\n")
267
+
268
+
269
+ if __name__ == "__main__":
270
+ main()
@@ -0,0 +1,138 @@
1
+ #!/usr/bin/env python3
2
+ # /// script
3
+ # requires-python = ">=3.11"
4
+ # ///
5
+ """Unit tests for resolve_personas.py — pool merge, alias, party resolution."""
6
+
7
+ import sys
8
+ import unittest
9
+ from pathlib import Path
10
+
11
+ sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
12
+ import resolve_personas as rp # noqa: E402
13
+
14
+ AGENTS = {
15
+ "bmad-agent-analyst": {"name": "Mary", "icon": "📊", "title": "Analyst"},
16
+ "bmad-agent-pm": {"name": "John", "icon": "📋", "title": "PM"},
17
+ }
18
+
19
+
20
+ class TestAlias(unittest.TestCase):
21
+ def test_strips_known_prefixes(self):
22
+ self.assertEqual(rp._alias("bmad-agent-analyst"), "analyst")
23
+ self.assertEqual(rp._alias("bmad-foo"), "foo")
24
+
25
+ def test_passes_through_unprefixed(self):
26
+ self.assertEqual(rp._alias("morpheus"), "morpheus")
27
+
28
+
29
+ class TestBuildPool(unittest.TestCase):
30
+ def test_installed_become_default_room_indexed_every_way(self):
31
+ pool, idx, installed, custom = rp.build_pool(AGENTS, [])
32
+ self.assertEqual(installed, ["bmad-agent-analyst", "bmad-agent-pm"])
33
+ self.assertEqual(custom, [])
34
+ self.assertEqual(idx["analyst"], "bmad-agent-analyst") # alias
35
+ self.assertEqual(idx["mary"], "bmad-agent-analyst") # name (ci)
36
+ self.assertEqual(pool["bmad-agent-analyst"]["source"], "installed")
37
+
38
+ def test_pure_custom_member_stays_out_of_default_room(self):
39
+ pool, _, installed, custom = rp.build_pool(
40
+ AGENTS, [{"code": "morpheus", "name": "Morpheus", "persona": "riddles"}])
41
+ self.assertEqual(custom, ["morpheus"])
42
+ self.assertNotIn("morpheus", installed)
43
+ self.assertEqual(pool["morpheus"]["persona"], "riddles")
44
+
45
+ def test_custom_override_lands_on_installed_slot_not_a_new_face(self):
46
+ pool, _, installed, custom = rp.build_pool(
47
+ AGENTS, [{"code": "analyst", "name": "Mary-Custom", "persona": "p"}])
48
+ self.assertNotIn("analyst", pool)
49
+ self.assertEqual(custom, []) # an override is not a new face
50
+ self.assertEqual(pool["bmad-agent-analyst"]["source"], "custom")
51
+ self.assertEqual(pool["bmad-agent-analyst"]["name"], "Mary-Custom")
52
+
53
+ def test_member_without_code_skipped(self):
54
+ pool, _, _, custom = rp.build_pool(AGENTS, [{"name": "Nameless"}])
55
+ self.assertEqual(custom, [])
56
+ self.assertEqual(set(pool), {"bmad-agent-analyst", "bmad-agent-pm"})
57
+
58
+ def test_custom_rename_does_not_hijack_another_agents_name(self):
59
+ # Override the analyst slot, renaming it to "John" — the PM's name.
60
+ # The PM's name lookup must survive (last-writer-wins would corrupt it).
61
+ _, idx, _, _ = rp.build_pool(AGENTS, [{"code": "analyst", "name": "John"}])
62
+ self.assertEqual(idx["john"], "bmad-agent-pm")
63
+
64
+ def test_brief_carries_model_and_capabilities(self):
65
+ pool, _, _, _ = rp.build_pool(
66
+ AGENTS, [{"code": "neo", "name": "Neo", "model": "opus", "capabilities": ["x"]}])
67
+ brief = rp._brief(pool["neo"])
68
+ self.assertEqual(brief["model"], "opus")
69
+ self.assertEqual(brief["capabilities"], ["x"])
70
+
71
+ def test_non_list_party_members_is_safe(self):
72
+ pool, _, installed, custom = rp.build_pool(AGENTS, "not-a-list")
73
+ self.assertEqual(custom, [])
74
+ self.assertEqual(set(pool), {"bmad-agent-analyst", "bmad-agent-pm"})
75
+
76
+
77
+ class TestResolveParties(unittest.TestCase):
78
+ def setUp(self):
79
+ self.pool, self.idx, _, _ = rp.build_pool(
80
+ AGENTS, [{"code": "shark", "name": "Marcus", "title": "CFO"}])
81
+
82
+ def test_resolves_members_by_alias_and_custom_code(self):
83
+ parties = rp.resolve_parties(
84
+ [{"id": "tank", "name": "Tank", "scene": "hostile",
85
+ "members": ["shark", "analyst"]}], self.pool, self.idx)
86
+ self.assertEqual(len(parties), 1)
87
+ self.assertEqual([m["name"] for m in parties[0]["members"]], ["Marcus", "Mary"])
88
+ self.assertEqual(parties[0]["scene"], "hostile")
89
+
90
+ def test_unknown_member_dropped_silently(self):
91
+ parties = rp.resolve_parties(
92
+ [{"id": "g", "members": ["analyst", "ghost"]}], self.pool, self.idx)
93
+ self.assertEqual([m["name"] for m in parties[0]["members"]], ["Mary"])
94
+
95
+ def test_member_resolution_is_case_insensitive(self):
96
+ # A TOML author naturally writes "Analyst"/"Shark"; the filter accepts
97
+ # them via the lowercase index, so resolution must too (no KeyError).
98
+ parties = rp.resolve_parties(
99
+ [{"id": "g", "members": ["Analyst", "Shark"]}], self.pool, self.idx)
100
+ self.assertEqual([m["name"] for m in parties[0]["members"]], ["Mary", "Marcus"])
101
+
102
+ def test_non_string_member_does_not_crash(self):
103
+ # Malformed members (int, list) must drop silently, never raise.
104
+ parties = rp.resolve_parties(
105
+ [{"id": "g", "members": [123, ["x"], "analyst"]}], self.pool, self.idx)
106
+ self.assertEqual([m["name"] for m in parties[0]["members"]], ["Mary"])
107
+
108
+ def test_open_cast_group_flagged(self):
109
+ parties = rp.resolve_parties(
110
+ [{"id": "rebels", "name": "Rebels", "scene": "the Ghost"}], self.pool, self.idx)
111
+ self.assertTrue(parties[0]["open_cast"])
112
+ self.assertEqual(parties[0]["members"], [])
113
+
114
+ def test_group_without_id_skipped(self):
115
+ self.assertEqual(rp.resolve_parties([{"name": "no id"}], self.pool, self.idx), [])
116
+
117
+
118
+ class TestOverrideMergeFallback(unittest.TestCase):
119
+ """When party-mode isn't installed, user override TOMLs are read directly."""
120
+
121
+ def test_arrays_append_scalars_override(self):
122
+ import tempfile, os
123
+ with tempfile.TemporaryDirectory() as d:
124
+ custom = Path(d) / "_bmad" / "custom"
125
+ custom.mkdir(parents=True)
126
+ (custom / "bmad-party-mode.toml").write_text(
127
+ '[workflow]\ndefault_party = "a"\n'
128
+ '[[workflow.party_members]]\ncode = "x"\nname = "X"\n')
129
+ (custom / "bmad-party-mode.user.toml").write_text(
130
+ '[workflow]\ndefault_party = "b"\n'
131
+ '[[workflow.party_members]]\ncode = "y"\nname = "Y"\n')
132
+ wf = rp.load_party_overrides(Path(d))
133
+ self.assertEqual(wf["default_party"], "b") # personal wins
134
+ self.assertEqual([m["code"] for m in wf["party_members"]], ["x", "y"]) # appended
135
+
136
+
137
+ if __name__ == "__main__":
138
+ unittest.main()