bmad-method 6.3.1-next.16 → 6.3.1-next.18

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 (39) hide show
  1. package/package.json +1 -1
  2. package/src/bmm-skills/1-analysis/bmad-document-project/workflows/deep-dive-instructions.md +1 -0
  3. package/src/bmm-skills/1-analysis/bmad-document-project/workflows/full-scan-instructions.md +1 -0
  4. package/src/bmm-skills/1-analysis/bmad-prfaq/customize.toml +22 -0
  5. package/src/bmm-skills/1-analysis/bmad-prfaq/references/verdict.md +4 -0
  6. package/src/bmm-skills/1-analysis/research/bmad-domain-research/customize.toml +22 -0
  7. package/src/bmm-skills/1-analysis/research/bmad-domain-research/domain-steps/step-06-research-synthesis.md +6 -0
  8. package/src/bmm-skills/1-analysis/research/bmad-market-research/customize.toml +26 -0
  9. package/src/bmm-skills/1-analysis/research/bmad-market-research/steps/step-06-research-completion.md +6 -0
  10. package/src/bmm-skills/1-analysis/research/bmad-technical-research/customize.toml +26 -0
  11. package/src/bmm-skills/1-analysis/research/bmad-technical-research/technical-steps/step-06-research-synthesis.md +6 -0
  12. package/src/bmm-skills/2-plan-workflows/bmad-create-prd/customize.toml +28 -1
  13. package/src/bmm-skills/2-plan-workflows/bmad-create-prd/steps-c/step-12-complete.md +6 -0
  14. package/src/bmm-skills/2-plan-workflows/bmad-create-ux-design/customize.toml +28 -1
  15. package/src/bmm-skills/2-plan-workflows/bmad-create-ux-design/steps/step-14-complete.md +6 -0
  16. package/src/bmm-skills/2-plan-workflows/bmad-edit-prd/customize.toml +29 -1
  17. package/src/bmm-skills/2-plan-workflows/bmad-edit-prd/steps-e/step-e-04-complete.md +2 -0
  18. package/src/bmm-skills/2-plan-workflows/bmad-validate-prd/customize.toml +29 -1
  19. package/src/bmm-skills/2-plan-workflows/bmad-validate-prd/steps-v/step-v-13-report-complete.md +1 -0
  20. package/src/bmm-skills/3-solutioning/bmad-check-implementation-readiness/customize.toml +28 -1
  21. package/src/bmm-skills/3-solutioning/bmad-check-implementation-readiness/steps/step-06-final-assessment.md +6 -0
  22. package/src/bmm-skills/3-solutioning/bmad-create-architecture/customize.toml +28 -1
  23. package/src/bmm-skills/3-solutioning/bmad-create-architecture/steps/step-08-complete.md +6 -0
  24. package/src/bmm-skills/3-solutioning/bmad-create-epics-and-stories/customize.toml +28 -1
  25. package/src/bmm-skills/3-solutioning/bmad-create-epics-and-stories/steps/step-04-final-validation.md +6 -0
  26. package/src/bmm-skills/3-solutioning/bmad-generate-project-context/customize.toml +28 -1
  27. package/src/bmm-skills/3-solutioning/bmad-generate-project-context/steps/step-03-complete.md +6 -0
  28. package/src/bmm-skills/4-implementation/bmad-correct-course/SKILL.md +1 -0
  29. package/src/bmm-skills/4-implementation/bmad-correct-course/customize.toml +28 -1
  30. package/src/bmm-skills/4-implementation/bmad-create-story/SKILL.md +1 -0
  31. package/src/bmm-skills/4-implementation/bmad-create-story/customize.toml +28 -1
  32. package/src/bmm-skills/4-implementation/bmad-qa-generate-e2e-tests/SKILL.md +6 -0
  33. package/src/bmm-skills/4-implementation/bmad-qa-generate-e2e-tests/customize.toml +28 -1
  34. package/src/bmm-skills/4-implementation/bmad-retrospective/SKILL.md +1 -1
  35. package/src/bmm-skills/4-implementation/bmad-retrospective/customize.toml +28 -1
  36. package/src/core-skills/bmad-customize/SKILL.md +111 -0
  37. package/src/core-skills/bmad-customize/scripts/list_customizable_skills.py +231 -0
  38. package/src/core-skills/bmad-customize/scripts/tests/test_list_customizable_skills.py +249 -0
  39. package/src/core-skills/module-help.csv +1 -0
@@ -0,0 +1,249 @@
1
+ #!/usr/bin/env python3
2
+ # /// script
3
+ # requires-python = ">=3.11"
4
+ # ///
5
+ """Unit tests for list_customizable_skills.py.
6
+
7
+ Exercises the scanner against a synthesized install tree:
8
+ - an agent-only customize.toml
9
+ - a workflow-only customize.toml
10
+ - a customize.toml that exposes both surfaces
11
+ - a skill directory with no customize.toml (ignored)
12
+ - a pre-existing team override in _bmad/custom/
13
+ - malformed TOML (surfaces as an error without aborting)
14
+ - multiple skills roots (e.g. project-local + user-global mix)
15
+
16
+ Run: python3 scripts/tests/test_list_customizable_skills.py
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import importlib.util
22
+ import json
23
+ import subprocess
24
+ import sys
25
+ import tempfile
26
+ import unittest
27
+ from pathlib import Path
28
+
29
+ SCRIPT = Path(__file__).resolve().parent.parent / "list_customizable_skills.py"
30
+
31
+
32
+ def _load_module():
33
+ spec = importlib.util.spec_from_file_location("list_customizable_skills", SCRIPT)
34
+ module = importlib.util.module_from_spec(spec)
35
+ spec.loader.exec_module(module) # type: ignore[union-attr]
36
+ return module
37
+
38
+
39
+ MODULE = _load_module()
40
+
41
+
42
+ def _make_skill(parent: Path, name: str, body: str, skill_md: str | None = None) -> Path:
43
+ skill_dir = parent / name
44
+ skill_dir.mkdir(parents=True, exist_ok=True)
45
+ (skill_dir / "customize.toml").write_text(body, encoding="utf-8")
46
+ if skill_md is not None:
47
+ (skill_dir / "SKILL.md").write_text(skill_md, encoding="utf-8")
48
+ return skill_dir
49
+
50
+
51
+ class ScannerTest(unittest.TestCase):
52
+ def setUp(self):
53
+ self.tmp = tempfile.TemporaryDirectory()
54
+ self.root = Path(self.tmp.name)
55
+ self.skills = self.root / "skills"
56
+ self.skills.mkdir(parents=True)
57
+ self.custom = self.root / "_bmad" / "custom"
58
+ self.custom.mkdir(parents=True)
59
+
60
+ def tearDown(self):
61
+ self.tmp.cleanup()
62
+
63
+ def test_agent_only_skill_detected(self):
64
+ _make_skill(
65
+ self.skills,
66
+ "bmad-agent-pm",
67
+ "[agent]\nicon = \"🧠\"\n",
68
+ "---\nname: bmad-agent-pm\ndescription: Product manager.\n---\n",
69
+ )
70
+ result = MODULE.scan_skills([self.skills], self.root)
71
+ self.assertEqual(len(result["agents"]), 1)
72
+ self.assertEqual(len(result["workflows"]), 0)
73
+ entry = result["agents"][0]
74
+ self.assertEqual(entry["name"], "bmad-agent-pm")
75
+ self.assertEqual(entry["surface"], "agent")
76
+ self.assertEqual(entry["description"], "Product manager.")
77
+ self.assertFalse(entry["has_team_override"])
78
+ self.assertFalse(entry["has_user_override"])
79
+
80
+ def test_workflow_only_skill_detected(self):
81
+ _make_skill(
82
+ self.skills,
83
+ "bmad-create-prd",
84
+ "[workflow]\npersistent_facts = []\n",
85
+ "---\nname: bmad-create-prd\ndescription: 'Create a PRD.'\n---\n",
86
+ )
87
+ result = MODULE.scan_skills([self.skills], self.root)
88
+ self.assertEqual(len(result["agents"]), 0)
89
+ self.assertEqual(len(result["workflows"]), 1)
90
+ entry = result["workflows"][0]
91
+ self.assertEqual(entry["description"], "Create a PRD.")
92
+
93
+ def test_dual_surface_skill_emits_two_entries(self):
94
+ _make_skill(
95
+ self.skills,
96
+ "bmad-dual",
97
+ "[agent]\nicon = \"x\"\n\n[workflow]\npersistent_facts = []\n",
98
+ "---\nname: bmad-dual\ndescription: Dual.\n---\n",
99
+ )
100
+ result = MODULE.scan_skills([self.skills], self.root)
101
+ self.assertEqual(len(result["agents"]), 1)
102
+ self.assertEqual(len(result["workflows"]), 1)
103
+ self.assertEqual(result["agents"][0]["name"], "bmad-dual")
104
+ self.assertEqual(result["workflows"][0]["name"], "bmad-dual")
105
+
106
+ def test_skill_without_customize_toml_ignored(self):
107
+ (self.skills / "bmad-plain").mkdir()
108
+ (self.skills / "bmad-plain" / "SKILL.md").write_text("# plain\n")
109
+ result = MODULE.scan_skills([self.skills], self.root)
110
+ self.assertEqual(len(result["agents"]) + len(result["workflows"]), 0)
111
+ self.assertEqual(result["errors"], [])
112
+
113
+ def test_existing_team_override_flagged(self):
114
+ _make_skill(
115
+ self.skills,
116
+ "bmad-agent-pm",
117
+ "[agent]\nicon = \"x\"\n",
118
+ "---\nname: bmad-agent-pm\ndescription: PM.\n---\n",
119
+ )
120
+ (self.custom / "bmad-agent-pm.toml").write_text("[agent]\n")
121
+ result = MODULE.scan_skills([self.skills], self.root)
122
+ entry = result["agents"][0]
123
+ self.assertTrue(entry["has_team_override"])
124
+ self.assertFalse(entry["has_user_override"])
125
+
126
+ def test_missing_surface_block_reports_error(self):
127
+ _make_skill(self.skills, "bmad-broken", "[not_a_surface]\nfoo = 1\n")
128
+ result = MODULE.scan_skills([self.skills], self.root)
129
+ self.assertEqual(len(result["agents"]) + len(result["workflows"]), 0)
130
+ self.assertEqual(len(result["errors"]), 1)
131
+ self.assertIn("no [agent] or [workflow] block", result["errors"][0])
132
+
133
+ def test_malformed_toml_reports_error_without_aborting(self):
134
+ skill_dir = self.skills / "bmad-bad"
135
+ skill_dir.mkdir()
136
+ (skill_dir / "customize.toml").write_text("this is not [valid toml\n")
137
+ # Plus a good sibling to confirm scanning continues.
138
+ _make_skill(
139
+ self.skills,
140
+ "bmad-good",
141
+ "[agent]\nicon = \"x\"\n",
142
+ "---\nname: bmad-good\ndescription: Good.\n---\n",
143
+ )
144
+ result = MODULE.scan_skills([self.skills], self.root)
145
+ self.assertEqual(len(result["agents"]), 1)
146
+ self.assertEqual(result["agents"][0]["name"], "bmad-good")
147
+ self.assertTrue(any("failed to parse" in e for e in result["errors"]))
148
+
149
+ def test_description_with_double_quotes_stripped(self):
150
+ _make_skill(
151
+ self.skills,
152
+ "bmad-q",
153
+ "[agent]\nicon = \"x\"\n",
154
+ '---\nname: bmad-q\ndescription: "Double-quoted desc."\n---\n',
155
+ )
156
+ result = MODULE.scan_skills([self.skills], self.root)
157
+ self.assertEqual(result["agents"][0]["description"], "Double-quoted desc.")
158
+
159
+ def test_multiple_skills_roots_are_merged(self):
160
+ extra_root = self.root / "extra-skills"
161
+ extra_root.mkdir()
162
+ _make_skill(
163
+ self.skills,
164
+ "bmad-agent-pm",
165
+ "[agent]\nicon = \"x\"\n",
166
+ "---\nname: bmad-agent-pm\ndescription: PM.\n---\n",
167
+ )
168
+ _make_skill(
169
+ extra_root,
170
+ "bmad-agent-dev",
171
+ "[agent]\nicon = \"y\"\n",
172
+ "---\nname: bmad-agent-dev\ndescription: Dev.\n---\n",
173
+ )
174
+ result = MODULE.scan_skills([self.skills, extra_root], self.root)
175
+ names = {a["name"] for a in result["agents"]}
176
+ self.assertEqual(names, {"bmad-agent-pm", "bmad-agent-dev"})
177
+ self.assertEqual(len(result["scanned_roots"]), 2)
178
+
179
+ def test_duplicate_skill_name_across_roots_first_wins(self):
180
+ extra_root = self.root / "extra-skills"
181
+ extra_root.mkdir()
182
+ _make_skill(
183
+ self.skills,
184
+ "bmad-agent-pm",
185
+ "[agent]\nicon = \"primary\"\n",
186
+ "---\nname: bmad-agent-pm\ndescription: Primary.\n---\n",
187
+ )
188
+ _make_skill(
189
+ extra_root,
190
+ "bmad-agent-pm",
191
+ "[agent]\nicon = \"duplicate\"\n",
192
+ "---\nname: bmad-agent-pm\ndescription: Duplicate.\n---\n",
193
+ )
194
+ result = MODULE.scan_skills([self.skills, extra_root], self.root)
195
+ self.assertEqual(len(result["agents"]), 1)
196
+ self.assertEqual(result["agents"][0]["description"], "Primary.")
197
+ self.assertEqual(result["agents"][0]["skills_root"], str(self.skills))
198
+
199
+ def test_missing_skills_root_reports_error(self):
200
+ result = MODULE.scan_skills(
201
+ [self.root / "does-not-exist", self.skills],
202
+ self.root,
203
+ )
204
+ self.assertTrue(any("skills root does not exist" in e for e in result["errors"]))
205
+
206
+ def test_cli_emits_valid_json_and_exits_zero(self):
207
+ _make_skill(
208
+ self.skills,
209
+ "bmad-agent-pm",
210
+ "[agent]\nicon = \"x\"\n",
211
+ "---\nname: bmad-agent-pm\ndescription: PM.\n---\n",
212
+ )
213
+ proc = subprocess.run(
214
+ [
215
+ sys.executable,
216
+ str(SCRIPT),
217
+ "--project-root",
218
+ str(self.root),
219
+ "--skills-root",
220
+ str(self.skills),
221
+ ],
222
+ capture_output=True,
223
+ text=True,
224
+ check=False,
225
+ )
226
+ self.assertEqual(proc.returncode, 0, proc.stderr)
227
+ payload = json.loads(proc.stdout)
228
+ self.assertEqual(len(payload["agents"]), 1)
229
+
230
+ def test_cli_exits_two_on_missing_project_root(self):
231
+ proc = subprocess.run(
232
+ [
233
+ sys.executable,
234
+ str(SCRIPT),
235
+ "--project-root",
236
+ str(self.root / "does-not-exist"),
237
+ "--skills-root",
238
+ str(self.skills),
239
+ ],
240
+ capture_output=True,
241
+ text=True,
242
+ check=False,
243
+ )
244
+ self.assertEqual(proc.returncode, 2)
245
+ self.assertIn("does not exist", proc.stderr)
246
+
247
+
248
+ if __name__ == "__main__":
249
+ unittest.main()
@@ -10,3 +10,4 @@ Core,bmad-editorial-review-structure,Editorial Review - Structure,ES,Use when do
10
10
  Core,bmad-review-adversarial-general,Adversarial Review,AR,"Use for quality assurance or before finalizing deliverables. Code Review in other modules runs this automatically, but also useful for document reviews.",[path],anytime,,,false,,
11
11
  Core,bmad-review-edge-case-hunter,Edge Case Hunter Review,ECH,Use alongside adversarial review for orthogonal coverage — method-driven not attitude-driven.,[path],anytime,,,false,,
12
12
  Core,bmad-distillator,Distillator,DG,Use when you need token-efficient distillates that preserve all information for downstream LLM consumption.,[path],anytime,,,false,adjacent to source document or specified output_path,distillate markdown file(s)
13
+ Core,bmad-customize,BMad Customize,BC,"Use when you want to change how an agent or workflow behaves — add persistent facts, swap templates, insert activation hooks, or customize menus. Scans what's customizable, picks the right scope (agent vs workflow), writes the override to _bmad/custom/, and verifies the merge. No TOML hand-authoring required.",,anytime,,,false,{project-root}/_bmad/custom,TOML override files