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.
- package/package.json +1 -1
- package/src/bmm-skills/1-analysis/bmad-document-project/workflows/deep-dive-instructions.md +1 -0
- package/src/bmm-skills/1-analysis/bmad-document-project/workflows/full-scan-instructions.md +1 -0
- package/src/bmm-skills/1-analysis/bmad-prfaq/customize.toml +22 -0
- package/src/bmm-skills/1-analysis/bmad-prfaq/references/verdict.md +4 -0
- package/src/bmm-skills/1-analysis/research/bmad-domain-research/customize.toml +22 -0
- package/src/bmm-skills/1-analysis/research/bmad-domain-research/domain-steps/step-06-research-synthesis.md +6 -0
- package/src/bmm-skills/1-analysis/research/bmad-market-research/customize.toml +26 -0
- package/src/bmm-skills/1-analysis/research/bmad-market-research/steps/step-06-research-completion.md +6 -0
- package/src/bmm-skills/1-analysis/research/bmad-technical-research/customize.toml +26 -0
- package/src/bmm-skills/1-analysis/research/bmad-technical-research/technical-steps/step-06-research-synthesis.md +6 -0
- package/src/bmm-skills/2-plan-workflows/bmad-create-prd/customize.toml +28 -1
- package/src/bmm-skills/2-plan-workflows/bmad-create-prd/steps-c/step-12-complete.md +6 -0
- package/src/bmm-skills/2-plan-workflows/bmad-create-ux-design/customize.toml +28 -1
- package/src/bmm-skills/2-plan-workflows/bmad-create-ux-design/steps/step-14-complete.md +6 -0
- package/src/bmm-skills/2-plan-workflows/bmad-edit-prd/customize.toml +29 -1
- package/src/bmm-skills/2-plan-workflows/bmad-edit-prd/steps-e/step-e-04-complete.md +2 -0
- package/src/bmm-skills/2-plan-workflows/bmad-validate-prd/customize.toml +29 -1
- package/src/bmm-skills/2-plan-workflows/bmad-validate-prd/steps-v/step-v-13-report-complete.md +1 -0
- package/src/bmm-skills/3-solutioning/bmad-check-implementation-readiness/customize.toml +28 -1
- package/src/bmm-skills/3-solutioning/bmad-check-implementation-readiness/steps/step-06-final-assessment.md +6 -0
- package/src/bmm-skills/3-solutioning/bmad-create-architecture/customize.toml +28 -1
- package/src/bmm-skills/3-solutioning/bmad-create-architecture/steps/step-08-complete.md +6 -0
- package/src/bmm-skills/3-solutioning/bmad-create-epics-and-stories/customize.toml +28 -1
- package/src/bmm-skills/3-solutioning/bmad-create-epics-and-stories/steps/step-04-final-validation.md +6 -0
- package/src/bmm-skills/3-solutioning/bmad-generate-project-context/customize.toml +28 -1
- package/src/bmm-skills/3-solutioning/bmad-generate-project-context/steps/step-03-complete.md +6 -0
- package/src/bmm-skills/4-implementation/bmad-correct-course/SKILL.md +1 -0
- package/src/bmm-skills/4-implementation/bmad-correct-course/customize.toml +28 -1
- package/src/bmm-skills/4-implementation/bmad-create-story/SKILL.md +1 -0
- package/src/bmm-skills/4-implementation/bmad-create-story/customize.toml +28 -1
- package/src/bmm-skills/4-implementation/bmad-qa-generate-e2e-tests/SKILL.md +6 -0
- package/src/bmm-skills/4-implementation/bmad-qa-generate-e2e-tests/customize.toml +28 -1
- package/src/bmm-skills/4-implementation/bmad-retrospective/SKILL.md +1 -1
- package/src/bmm-skills/4-implementation/bmad-retrospective/customize.toml +28 -1
- package/src/core-skills/bmad-customize/SKILL.md +111 -0
- package/src/core-skills/bmad-customize/scripts/list_customizable_skills.py +231 -0
- package/src/core-skills/bmad-customize/scripts/tests/test_list_customizable_skills.py +249 -0
- 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
|