bmad-method 6.11.1-next.8 → 6.12.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 (153) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/AGENTS.md +12 -0
  3. package/CLAUDE.md +1 -0
  4. package/README.md +13 -19
  5. package/README_CN.md +0 -10
  6. package/README_KR.md +90 -0
  7. package/README_VN.md +0 -10
  8. package/greptile.json +52 -0
  9. package/package.json +6 -5
  10. package/src/bmm-skills/agents/bmad-agent-analyst/SKILL.md +1 -1
  11. package/src/bmm-skills/agents/bmad-agent-analyst/customize.toml +1 -3
  12. package/src/bmm-skills/agents/bmad-agent-architect/SKILL.md +1 -1
  13. package/src/bmm-skills/agents/bmad-agent-architect/customize.toml +1 -3
  14. package/src/bmm-skills/agents/bmad-agent-dev/SKILL.md +1 -1
  15. package/src/bmm-skills/agents/bmad-agent-dev/customize.toml +1 -3
  16. package/src/bmm-skills/agents/bmad-agent-pm/SKILL.md +1 -1
  17. package/src/bmm-skills/agents/bmad-agent-pm/customize.toml +1 -3
  18. package/src/bmm-skills/agents/bmad-agent-ux-designer/SKILL.md +1 -1
  19. package/src/bmm-skills/agents/bmad-agent-ux-designer/customize.toml +1 -3
  20. package/src/bmm-skills/module-help.csv +2 -2
  21. package/src/bmm-skills/plan/bmad-architecture/SKILL.md +1 -1
  22. package/src/bmm-skills/plan/bmad-architecture/customize.toml +5 -6
  23. package/src/bmm-skills/plan/bmad-create-epics-and-stories/SKILL.md +1 -1
  24. package/src/bmm-skills/plan/bmad-create-epics-and-stories/customize.toml +1 -3
  25. package/src/bmm-skills/plan/bmad-create-epics-and-stories/steps/step-04-final-validation.md +1 -1
  26. package/src/bmm-skills/plan/bmad-prd/SKILL.md +1 -1
  27. package/src/bmm-skills/plan/bmad-prd/customize.toml +5 -6
  28. package/src/bmm-skills/plan/bmad-prfaq/SKILL.md +1 -1
  29. package/src/bmm-skills/plan/bmad-prfaq/customize.toml +1 -3
  30. package/src/bmm-skills/plan/bmad-prfaq/references/verdict.md +1 -1
  31. package/src/bmm-skills/plan/bmad-product-brief/SKILL.md +1 -1
  32. package/src/bmm-skills/plan/bmad-product-brief/customize.toml +5 -6
  33. package/src/bmm-skills/plan/bmad-project-context/SKILL.md +7 -6
  34. package/src/bmm-skills/plan/bmad-project-context/references/best-practices.md +2 -2
  35. package/src/bmm-skills/plan/bmad-spec/SKILL.md +1 -1
  36. package/src/bmm-skills/plan/bmad-spec/customize.toml +5 -5
  37. package/src/bmm-skills/plan/bmad-sprint-planning/SKILL.md +1 -1
  38. package/src/bmm-skills/plan/bmad-sprint-planning/customize.toml +1 -3
  39. package/src/bmm-skills/plan/bmad-sprint-planning/scripts/__pycache__/sprint_plan.cpython-311.pyc +0 -0
  40. package/src/bmm-skills/plan/bmad-sprint-planning/scripts/tests/__pycache__/test_sprint_plan.cpython-311-pytest-9.1.1.pyc +0 -0
  41. package/src/bmm-skills/plan/bmad-ux/SKILL.md +1 -1
  42. package/src/bmm-skills/plan/bmad-ux/customize.toml +1 -3
  43. package/src/bmm-skills/ship/bmad-build/SKILL.md +1 -1
  44. package/src/bmm-skills/ship/bmad-build/customize.toml +21 -33
  45. package/src/bmm-skills/ship/bmad-build/references/claims-check.md +14 -0
  46. package/src/bmm-skills/ship/bmad-build/review-prompts/edge-case-hunter.md +31 -7
  47. package/src/bmm-skills/ship/bmad-build/review-prompts/verification-gap.md +4 -4
  48. package/src/bmm-skills/ship/bmad-build/spec-template.md +26 -4
  49. package/src/bmm-skills/ship/bmad-build/step-01-clarify-and-route.md +21 -26
  50. package/src/bmm-skills/ship/bmad-build/step-02-plan.md +31 -16
  51. package/src/bmm-skills/ship/bmad-build/step-03-implement.md +5 -3
  52. package/src/bmm-skills/ship/bmad-build/step-04-review.md +49 -19
  53. package/src/bmm-skills/ship/bmad-build/step-05-present.md +9 -41
  54. package/src/bmm-skills/ship/bmad-build/step-oneshot.md +60 -29
  55. package/src/bmm-skills/ship/bmad-build/sync-sprint-status.md +5 -19
  56. package/src/bmm-skills/ship/bmad-build-auto/customize.toml +17 -19
  57. package/src/bmm-skills/ship/bmad-build-auto/references/claims-check.md +14 -0
  58. package/src/bmm-skills/ship/bmad-build-auto/review-prompts/edge-case-hunter.md +31 -7
  59. package/src/bmm-skills/ship/bmad-build-auto/review-prompts/verification-gap.md +4 -4
  60. package/src/bmm-skills/ship/bmad-build-auto/spec-template.md +4 -7
  61. package/src/bmm-skills/ship/bmad-build-auto/step-01-clarify-and-route.md +3 -2
  62. package/src/bmm-skills/ship/bmad-build-auto/step-03-implement.md +5 -1
  63. package/src/bmm-skills/ship/bmad-build-auto/step-04-review.md +58 -38
  64. package/src/bmm-skills/ship/bmad-build-auto/workflow.md +1 -1
  65. package/src/bmm-skills/ship/bmad-code-review/SKILL.md +1 -1
  66. package/src/bmm-skills/ship/bmad-code-review/customize.toml +15 -18
  67. package/src/bmm-skills/ship/bmad-code-review/references/claims-check.md +14 -0
  68. package/src/bmm-skills/ship/bmad-code-review/review-prompts/edge-case-hunter.md +31 -7
  69. package/src/bmm-skills/ship/bmad-code-review/review-prompts/verification-gap.md +4 -4
  70. package/src/bmm-skills/ship/bmad-code-review/steps/step-01-gather-context.md +30 -23
  71. package/src/bmm-skills/ship/bmad-code-review/steps/step-02-review.md +3 -3
  72. package/src/bmm-skills/ship/bmad-code-review/steps/step-03-triage.md +25 -21
  73. package/src/bmm-skills/ship/bmad-code-review/steps/step-04-present.md +20 -18
  74. package/src/bmm-skills/ship/bmad-correct-course/SKILL.md +13 -5
  75. package/src/bmm-skills/ship/bmad-correct-course/customize.toml +1 -3
  76. package/src/bmm-skills/ship/bmad-qa-generate-e2e-tests/SKILL.md +2 -2
  77. package/src/bmm-skills/ship/bmad-qa-generate-e2e-tests/customize.toml +1 -3
  78. package/src/bmm-skills/ship/bmad-retrospective/SKILL.md +1 -1
  79. package/src/bmm-skills/ship/bmad-retrospective/customize.toml +1 -3
  80. package/src/bmm-skills/ship/bmad-retrospective/scripts/__pycache__/sprint_status.cpython-311.pyc +0 -0
  81. package/src/bmm-skills/ship/bmad-retrospective/scripts/tests/__pycache__/test_git_evidence.cpython-311-pytest-9.1.1.pyc +0 -0
  82. package/src/bmm-skills/ship/bmad-retrospective/scripts/tests/__pycache__/test_sprint_status.cpython-311-pytest-9.1.1.pyc +0 -0
  83. package/src/bmm-skills/ship/{bmad-checkpoint-preview → bmad-walkthrough}/SKILL.md +4 -4
  84. package/src/bmm-skills/ship/{bmad-checkpoint-preview → bmad-walkthrough}/customize.toml +2 -4
  85. package/src/bmm-skills/ship/{bmad-checkpoint-preview → bmad-walkthrough}/step-05-wrapup.md +1 -1
  86. package/src/bmm-skills/v6-shims/README.md +1 -0
  87. package/src/bmm-skills/v6-shims/bmad-checkpoint-preview/SKILL.md +21 -0
  88. package/src/bmm-skills/v6-shims/bmad-create-architecture/SKILL.md +1 -1
  89. package/src/bmm-skills/v6-shims/bmad-create-architecture/customize.toml +1 -3
  90. package/src/bmm-skills/v6-shims/bmad-create-prd/SKILL.md +1 -1
  91. package/src/bmm-skills/v6-shims/bmad-create-prd/customize.toml +1 -3
  92. package/src/bmm-skills/v6-shims/bmad-create-story/SKILL.md +3 -3
  93. package/src/bmm-skills/v6-shims/bmad-create-story/customize.toml +1 -3
  94. package/src/bmm-skills/v6-shims/bmad-dev-story/SKILL.md +2 -2
  95. package/src/bmm-skills/v6-shims/bmad-dev-story/customize.toml +1 -6
  96. package/src/bmm-skills/v6-shims/bmad-domain-research/SKILL.md +1 -1
  97. package/src/bmm-skills/v6-shims/bmad-edit-prd/SKILL.md +1 -1
  98. package/src/bmm-skills/v6-shims/bmad-edit-prd/customize.toml +1 -3
  99. package/src/bmm-skills/v6-shims/bmad-market-research/SKILL.md +1 -1
  100. package/src/bmm-skills/v6-shims/bmad-sprint-status/SKILL.md +1 -1
  101. package/src/bmm-skills/v6-shims/bmad-sprint-status/customize.toml +1 -3
  102. package/src/bmm-skills/v6-shims/bmad-technical-research/SKILL.md +1 -1
  103. package/src/bmm-skills/v6-shims/bmad-validate-prd/SKILL.md +1 -1
  104. package/src/bmm-skills/v6-shims/bmad-validate-prd/customize.toml +1 -3
  105. package/src/core-skills/bmad-advanced-elicitation/SKILL.md +22 -23
  106. package/src/core-skills/bmad-brainstorming/SKILL.md +1 -1
  107. package/src/core-skills/bmad-brainstorming/customize.toml +5 -6
  108. package/src/core-skills/bmad-brainstorming/scripts/brain.py +19 -0
  109. package/src/core-skills/bmad-brainstorming/scripts/tests/test_brain.py +50 -0
  110. package/src/core-skills/bmad-customize/SKILL.md +1 -1
  111. package/src/core-skills/bmad-deep-recon/SKILL.md +1 -1
  112. package/src/core-skills/bmad-forge-idea/SKILL.md +1 -1
  113. package/src/core-skills/bmad-forge-idea/customize.toml +5 -6
  114. package/src/core-skills/bmad-forge-idea/scripts/resolve_personas.py +3 -1
  115. package/src/core-skills/bmad-forge-idea/scripts/tests/test_resolve_personas.py +22 -0
  116. package/src/core-skills/bmad-help/SKILL.md +1 -1
  117. package/src/core-skills/bmad-party-mode/SKILL.md +1 -1
  118. package/src/core-skills/bmad-party-mode/customize.toml +4 -4
  119. package/src/core-skills/bmad-party-mode/references/create-party.md +1 -1
  120. package/src/core-skills/bmad-party-mode/scripts/resolve_party.py +3 -1
  121. package/src/core-skills/bmad-party-mode/scripts/tests/test_resolve_party.py +22 -0
  122. package/src/core-skills/bmad-review/SKILL.md +5 -4
  123. package/src/core-skills/bmad-review/customize.toml +1 -1
  124. package/src/core-skills/bmad-review/references/lens-edge-case-hunter.md +18 -0
  125. package/src/core-skills/module-help.csv +1 -1
  126. package/src/scripts/__pycache__/config_utils.cpython-311.pyc +0 -0
  127. package/src/scripts/resolve_config.py +9 -1
  128. package/src/scripts/resolve_customization.py +72 -7
  129. package/src/scripts/tests/__pycache__/test_config_utils.cpython-311.pyc +0 -0
  130. package/src/scripts/tests/__pycache__/test_resolve_config.cpython-311.pyc +0 -0
  131. package/src/scripts/tests/__pycache__/test_resolve_customization.cpython-311.pyc +0 -0
  132. package/src/scripts/tests/test_resolve_config.py +28 -0
  133. package/src/scripts/tests/test_resolve_customization.py +102 -0
  134. package/tools/installer/core/installer.js +33 -1
  135. package/tools/installer/core/shim-policy.js +75 -7
  136. package/tools/installer/ide/platform-codes.yaml +13 -0
  137. package/tools/installer/prompts.js +15 -2
  138. package/tools/installer/ui.js +28 -8
  139. package/tools/skill-validator.md +85 -151
  140. package/tools/tests/__pycache__/test_validate_skills.cpython-311.pyc +0 -0
  141. package/tools/tests/fixtures/validate-skills/bmad/SKILL.md +8 -0
  142. package/tools/tests/fixtures/validate-skills/deprecated-shim/SKILL.md +9 -0
  143. package/tools/tests/fixtures/validate-skills/missing-trigger/SKILL.md +9 -0
  144. package/tools/tests/fixtures/validate-skills/with-trigger/SKILL.md +8 -0
  145. package/tools/tests/test_validate_skills.py +476 -0
  146. package/tools/validate-published-implementation-model.mjs +0 -9
  147. package/tools/validate_skills.py +698 -0
  148. package/tools/validate-skills.js +0 -735
  149. /package/src/bmm-skills/ship/{bmad-checkpoint-preview → bmad-walkthrough}/generate-trail.md +0 -0
  150. /package/src/bmm-skills/ship/{bmad-checkpoint-preview → bmad-walkthrough}/step-01-orientation.md +0 -0
  151. /package/src/bmm-skills/ship/{bmad-checkpoint-preview → bmad-walkthrough}/step-02-walkthrough.md +0 -0
  152. /package/src/bmm-skills/ship/{bmad-checkpoint-preview → bmad-walkthrough}/step-03-detail-pass.md +0 -0
  153. /package/src/bmm-skills/ship/{bmad-checkpoint-preview → bmad-walkthrough}/step-04-testing.md +0 -0
@@ -0,0 +1,476 @@
1
+ import contextlib
2
+ import importlib.util
3
+ import io
4
+ import json
5
+ import os
6
+ import re
7
+ import sys
8
+ import tempfile
9
+ import unittest
10
+ from pathlib import Path
11
+ from unittest import mock
12
+
13
+
14
+ REPO_ROOT = Path(__file__).resolve().parents[2]
15
+ VALIDATOR = REPO_ROOT / "tools" / "validate_skills.py"
16
+ FIXTURES = REPO_ROOT / "tools" / "tests" / "fixtures" / "validate-skills"
17
+
18
+ JS_JSON_MISSING_TRIGGER = [
19
+ {
20
+ "skill": "tools/tests/fixtures/validate-skills/missing-trigger",
21
+ "rule": "SKILL-04",
22
+ "title": "name Format",
23
+ "severity": "HIGH",
24
+ "file": "SKILL.md",
25
+ "line": None,
26
+ "detail": 'name "missing-trigger" does not match pattern: /^(?:bmad|bmad-[a-z0-9]+(?:-[a-z0-9]+)*)$/',
27
+ "fix": "Rename to comply with lowercase letters, numbers, and hyphens only (max 64 chars).",
28
+ },
29
+ {
30
+ "skill": "tools/tests/fixtures/validate-skills/missing-trigger",
31
+ "rule": "SKILL-06",
32
+ "title": "description Quality",
33
+ "severity": "MEDIUM",
34
+ "file": "SKILL.md",
35
+ "line": None,
36
+ "detail": 'description does not contain "Use when" or "Use if" trigger phrase.',
37
+ "fix": 'Append a "Use when..." clause to explain when to invoke this skill.',
38
+ },
39
+ ]
40
+
41
+
42
+ def load_validator():
43
+ sys.dont_write_bytecode = True
44
+ spec = importlib.util.spec_from_file_location("validate_skills", VALIDATOR)
45
+ module = importlib.util.module_from_spec(spec)
46
+ assert spec.loader is not None
47
+ spec.loader.exec_module(module)
48
+ return module
49
+
50
+
51
+ vs = load_validator()
52
+
53
+
54
+ def write(path: Path, content: str) -> None:
55
+ path.parent.mkdir(parents=True, exist_ok=True)
56
+ path.write_text(content, encoding="utf-8")
57
+
58
+
59
+ def skill_md(name: str, description: str, body: str = "# Body\n\nDo the thing.\n", extra_fm: str = "") -> str:
60
+ return f"---\nname: {name}\ndescription: '{description}'\n{extra_fm}---\n\n{body}"
61
+
62
+
63
+ def findings_by_rule(findings: list[dict], rule: str) -> list[dict]:
64
+ return [f for f in findings if f["rule"] == rule]
65
+
66
+
67
+ class FixtureCase(unittest.TestCase):
68
+ def findings(self, name: str) -> list[dict]:
69
+ return vs.validate_skill(str(FIXTURES / name))
70
+
71
+ def has_trigger_finding(self, name: str) -> bool:
72
+ return any(
73
+ f["rule"] == "SKILL-06" and re.search(r"trigger phrase", f["detail"], re.I)
74
+ for f in self.findings(name)
75
+ )
76
+
77
+ def test_deprecated_skill_is_exempt_from_trigger_phrase(self):
78
+ self.assertFalse(self.has_trigger_finding("deprecated-shim"))
79
+
80
+ def test_active_skill_missing_trigger_phrase_is_flagged(self):
81
+ self.assertTrue(self.has_trigger_finding("missing-trigger"))
82
+
83
+ def test_active_skill_with_use_when_is_not_flagged(self):
84
+ self.assertFalse(self.has_trigger_finding("with-trigger"))
85
+
86
+ def test_canonical_bmad_root_skill_satisfies_name_format(self):
87
+ self.assertFalse(any(f["rule"] == "SKILL-04" for f in self.findings("bmad")))
88
+
89
+ def _json_for(self, name: str) -> list[dict]:
90
+ out = io.StringIO()
91
+ with contextlib.redirect_stdout(out):
92
+ code = vs.main(["--json", str(FIXTURES / name)])
93
+ self.assertEqual(code, 0)
94
+ return json.loads(out.getvalue())
95
+
96
+ def test_json_matches_js_for_missing_trigger(self):
97
+ self.assertEqual(self._json_for("missing-trigger"), JS_JSON_MISSING_TRIGGER)
98
+
99
+ def test_json_matches_js_for_other_fixtures(self):
100
+ deprecated = self._json_for("deprecated-shim")
101
+ self.assertEqual(len(deprecated), 1)
102
+ self.assertEqual(deprecated[0]["rule"], "SKILL-04")
103
+ self.assertEqual(deprecated[0]["skill"], "tools/tests/fixtures/validate-skills/deprecated-shim")
104
+
105
+ with_trigger = self._json_for("with-trigger")
106
+ self.assertEqual(len(with_trigger), 1)
107
+ self.assertEqual(with_trigger[0]["rule"], "SKILL-04")
108
+ self.assertFalse(
109
+ any(f["rule"] == "SKILL-06" and re.search(r"trigger phrase", f["detail"], re.I) for f in with_trigger)
110
+ )
111
+
112
+ self.assertEqual(self._json_for("bmad"), [])
113
+
114
+
115
+ class ProjectCase(unittest.TestCase):
116
+ def setUp(self):
117
+ self._tmp = tempfile.TemporaryDirectory()
118
+ self.addCleanup(self._tmp.cleanup)
119
+ self.root = Path(self._tmp.name)
120
+ self.skills = self.root / "src"
121
+ self.skills.mkdir()
122
+ patcher = mock.patch.dict(os.environ, clear=False)
123
+ patcher.start()
124
+ self.addCleanup(patcher.stop)
125
+ os.environ.pop("GITHUB_ACTIONS", None)
126
+ os.environ.pop("GITHUB_STEP_SUMMARY", None)
127
+
128
+ def add_skill(self, dirname: str, content: str, files: dict[str, str] | None = None) -> Path:
129
+ skill = self.skills / dirname
130
+ write(skill / "SKILL.md", content)
131
+ if files:
132
+ for rel, text in files.items():
133
+ write(skill / rel, text)
134
+ return skill
135
+
136
+ def valid(self, dirname: str, files: dict[str, str] | None = None) -> Path:
137
+ return self.add_skill(
138
+ dirname,
139
+ skill_md(dirname, f"Helps with {dirname}. Use when the user needs {dirname}."),
140
+ files,
141
+ )
142
+
143
+ def run_validator(self, skill_dir=None, strict=False, json_output=False):
144
+ out = io.StringIO()
145
+ err = io.StringIO()
146
+ with contextlib.redirect_stdout(out), contextlib.redirect_stderr(err):
147
+ code = vs.run(
148
+ str(self.root),
149
+ skill_dir=None if skill_dir is None else str(skill_dir),
150
+ strict=strict,
151
+ json_output=json_output,
152
+ )
153
+ return code, out.getvalue(), err.getvalue()
154
+
155
+ def findings_for(self, skill: Path) -> list[dict]:
156
+ return vs.validate_skill(str(skill))
157
+
158
+
159
+ class TestRules(ProjectCase):
160
+ def test_skill_01_missing_skill_md(self):
161
+ skill = self.skills / "bmad-empty"
162
+ skill.mkdir()
163
+ findings = self.findings_for(skill)
164
+ self.assertEqual([f["rule"] for f in findings], ["SKILL-01"])
165
+ self.assertEqual(findings[0]["severity"], "CRITICAL")
166
+ self.assertEqual(findings[0]["detail"], "SKILL.md not found in skill directory.")
167
+
168
+ def test_skill_02_name_absent_vs_empty(self):
169
+ absent = self.add_skill(
170
+ "bmad-no-name",
171
+ "---\ndescription: 'Does a thing. Use when testing.'\n---\n\n# Body\n",
172
+ )
173
+ empty = self.add_skill(
174
+ "bmad-empty-name",
175
+ "---\nname: ''\ndescription: 'Does a thing. Use when testing.'\n---\n\n# Body\n",
176
+ )
177
+ absent_f = findings_by_rule(self.findings_for(absent), "SKILL-02")
178
+ empty_f = findings_by_rule(self.findings_for(empty), "SKILL-02")
179
+ self.assertEqual(len(absent_f), 1)
180
+ self.assertEqual(absent_f[0]["detail"], "Frontmatter is missing the `name` field.")
181
+ self.assertEqual(absent_f[0]["fix"], "Add `name: <skill-name>` to the frontmatter.")
182
+ self.assertEqual(len(empty_f), 1)
183
+ self.assertEqual(empty_f[0]["detail"], "Frontmatter `name` field is empty.")
184
+ self.assertEqual(empty_f[0]["fix"], "Set `name` to the skill directory name (kebab-case).")
185
+
186
+ def test_skill_03_description_absent_vs_empty(self):
187
+ absent = self.add_skill("bmad-no-desc", "---\nname: bmad-no-desc\n---\n\n# Body\n")
188
+ empty = self.add_skill("bmad-empty-desc", "---\nname: bmad-empty-desc\ndescription: ''\n---\n\n# Body\n")
189
+ absent_f = findings_by_rule(self.findings_for(absent), "SKILL-03")
190
+ empty_f = findings_by_rule(self.findings_for(empty), "SKILL-03")
191
+ self.assertEqual(len(absent_f), 1)
192
+ self.assertEqual(absent_f[0]["detail"], "Frontmatter is missing the `description` field.")
193
+ self.assertEqual(absent_f[0]["severity"], "CRITICAL")
194
+ self.assertEqual(len(empty_f), 1)
195
+ self.assertEqual(empty_f[0]["detail"], "Frontmatter `description` field is empty.")
196
+
197
+ def test_skill_03_removed_description_strict_exits_1(self):
198
+ skill = self.add_skill("bmad-canary", "---\nname: bmad-canary\n---\n\n# Body\n")
199
+ code, out, _ = self.run_validator(skill_dir=skill, strict=True)
200
+ self.assertEqual(code, 1)
201
+ self.assertIn("[CRITICAL] SKILL-03", out)
202
+
203
+ def test_skill_04_invalid_name_uses_js_regex_literal(self):
204
+ skill = self.add_skill(
205
+ "bmad-bad",
206
+ skill_md("NotValid", "Does a thing. Use when testing name format."),
207
+ )
208
+ findings = findings_by_rule(self.findings_for(skill), "SKILL-04")
209
+ self.assertEqual(len(findings), 1)
210
+ self.assertEqual(findings[0]["severity"], "HIGH")
211
+ self.assertEqual(
212
+ findings[0]["detail"],
213
+ 'name "NotValid" does not match pattern: /^(?:bmad|bmad-[a-z0-9]+(?:-[a-z0-9]+)*)$/',
214
+ )
215
+
216
+ def test_skill_05_name_must_match_directory(self):
217
+ skill = self.add_skill(
218
+ "bmad-dir",
219
+ skill_md("bmad-other", "Does a thing. Use when testing directory match."),
220
+ )
221
+ findings = findings_by_rule(self.findings_for(skill), "SKILL-05")
222
+ self.assertEqual(len(findings), 1)
223
+ self.assertEqual(
224
+ findings[0]["detail"],
225
+ 'name "bmad-other" does not match directory name "bmad-dir".',
226
+ )
227
+
228
+ def test_skill_06_length_and_trigger_subchecks(self):
229
+ long_desc = "Use when testing. " + ("x" * 1024)
230
+ long_skill = self.add_skill("bmad-long", skill_md("bmad-long", long_desc))
231
+ length = findings_by_rule(self.findings_for(long_skill), "SKILL-06")
232
+ self.assertEqual(len(length), 1)
233
+ self.assertIn("characters (max 1024)", length[0]["detail"])
234
+ self.assertNotRegex(length[0]["detail"], re.compile("trigger phrase", re.I))
235
+
236
+ missing = self.add_skill(
237
+ "bmad-no-trigger",
238
+ skill_md("bmad-no-trigger", "Generates a thing and writes it to disk."),
239
+ )
240
+ trigger = findings_by_rule(self.findings_for(missing), "SKILL-06")
241
+ self.assertEqual(len(trigger), 1)
242
+ self.assertIn("trigger phrase", trigger[0]["detail"])
243
+
244
+ exact = self.add_skill(
245
+ "bmad-exact",
246
+ skill_md("bmad-exact", ("Use when x. " + ("y" * (1024 - len("Use when x. "))))),
247
+ )
248
+ self.assertEqual(findings_by_rule(self.findings_for(exact), "SKILL-06"), [])
249
+
250
+ def test_skill_06_deprecated_and_use_if_exempt(self):
251
+ deprecated = self.add_skill(
252
+ "bmad-old",
253
+ skill_md("bmad-old", "DEPRECATED — use bmad-new instead."),
254
+ )
255
+ self.assertEqual(findings_by_rule(self.findings_for(deprecated), "SKILL-06"), [])
256
+
257
+ use_if = self.add_skill(
258
+ "bmad-if",
259
+ skill_md("bmad-if", "Does a thing. Use if the user already has a file."),
260
+ )
261
+ self.assertEqual(findings_by_rule(self.findings_for(use_if), "SKILL-06"), [])
262
+
263
+ def test_skill_07_empty_body_and_unclosed_frontmatter(self):
264
+ empty = self.add_skill(
265
+ "bmad-nobody",
266
+ "---\nname: bmad-nobody\ndescription: 'Does a thing. Use when testing.'\n---\n",
267
+ )
268
+ unclosed = self.add_skill(
269
+ "bmad-unclosed",
270
+ "---\nname: bmad-unclosed\ndescription: 'Does a thing. Use when testing.'\n",
271
+ )
272
+ empty_f = findings_by_rule(self.findings_for(empty), "SKILL-07")
273
+ unclosed_f = findings_by_rule(self.findings_for(unclosed), "SKILL-07")
274
+ self.assertEqual(len(empty_f), 1)
275
+ self.assertEqual(empty_f[0]["severity"], "HIGH")
276
+ self.assertEqual(len(unclosed_f), 1)
277
+
278
+ def test_path_02_frontmatter_key_and_content_line(self):
279
+ skill = self.valid(
280
+ "bmad-path",
281
+ {
282
+ "notes.md": "---\ninstalled_path: .\n---\n\nSee {installed_path}/foo.md\n",
283
+ },
284
+ )
285
+ findings = findings_by_rule(self.findings_for(skill), "PATH-02")
286
+ self.assertGreaterEqual(len(findings), 2)
287
+ self.assertTrue(any("Frontmatter contains `installed_path:` key." == f["detail"] for f in findings))
288
+ self.assertTrue(any(f.get("line") and "reference found in content" in f["detail"] for f in findings))
289
+
290
+ def test_path_02_ignores_code_blocks(self):
291
+ skill = self.valid(
292
+ "bmad-path-code",
293
+ {"notes.md": "```\ninstalled_path\n```\n"},
294
+ )
295
+ self.assertEqual(findings_by_rule(self.findings_for(skill), "PATH-02"), [])
296
+
297
+ def test_seq_02_patterns_one_per_line_and_eta_case(self):
298
+ skill = self.valid(
299
+ "bmad-seq",
300
+ {
301
+ "workflow.md": "\n".join(
302
+ [
303
+ "This takes 5 min to run",
304
+ "~ 10 min leftover",
305
+ "See estimated time below",
306
+ "Ship ETA tomorrow",
307
+ "lowercase eta is fine",
308
+ "takes 3 min and estimated time on one line",
309
+ ]
310
+ )
311
+ + "\n",
312
+ },
313
+ )
314
+ findings = findings_by_rule(self.findings_for(skill), "SEQ-02")
315
+ lines = sorted(f["line"] for f in findings)
316
+ self.assertEqual(lines, [1, 2, 3, 4, 6])
317
+ self.assertFalse(any(f["line"] == 5 for f in findings))
318
+ self.assertEqual(sum(1 for f in findings if f["line"] == 6), 1)
319
+
320
+ def test_tpl_01_does_not_strip_code_blocks(self):
321
+ skill = self.valid(
322
+ "bmad-tpl",
323
+ {
324
+ "template.md": "plain {{.name}}\n```\nfenced {{.other}}\n```\n",
325
+ "notes.md": "{{.ignored}}\n",
326
+ },
327
+ )
328
+ findings = findings_by_rule(self.findings_for(skill), "TPL-01")
329
+ self.assertEqual(len(findings), 2)
330
+ self.assertEqual({f["line"] for f in findings}, {1, 3})
331
+ self.assertTrue(any("{{.name}}" in f["detail"] for f in findings))
332
+ self.assertTrue(any("{{.other}}" in f["detail"] for f in findings))
333
+
334
+ def test_read_err_on_unreadable_file_continues(self):
335
+ skill = self.valid("bmad-perm", {"secret.md": "ok\n"})
336
+ target = skill / "secret.md"
337
+ os.chmod(target, 0)
338
+ self.addCleanup(os.chmod, target, 0o644)
339
+ findings = findings_by_rule(self.findings_for(skill), "READ-ERR")
340
+ self.assertGreaterEqual(len(findings), 1)
341
+ self.assertEqual(findings[0]["severity"], "MEDIUM")
342
+ self.assertTrue(findings[0]["detail"].startswith("Cannot read file:"))
343
+
344
+
345
+ class TestCliAndOutput(ProjectCase):
346
+ def test_clean_tree_strict_exits_zero(self):
347
+ self.valid("bmad-clean")
348
+ code, out, err = self.run_validator(strict=True)
349
+ self.assertEqual(code, 0)
350
+ self.assertEqual(err, "")
351
+ self.assertIn("Skills scanned: 1", out)
352
+ self.assertIn("Total findings: 0", out)
353
+ self.assertIn("All skills passed validation!", out)
354
+
355
+ def test_strict_high_plus_exits_one(self):
356
+ self.add_skill("bmad-high", skill_md("Nope", "Does a thing. Use when testing."))
357
+ code, out, _ = self.run_validator(strict=True)
358
+ self.assertEqual(code, 1)
359
+ self.assertIn("[STRICT MODE] HIGH+ findings found — exiting with failure.", out)
360
+
361
+ def test_strict_medium_only_exits_zero(self):
362
+ self.add_skill("bmad-med", skill_md("bmad-med", "Generates a thing with no trigger."))
363
+ code, out, _ = self.run_validator(strict=True)
364
+ self.assertEqual(code, 0)
365
+ self.assertIn("[STRICT MODE] Only MEDIUM/LOW findings — pass.", out)
366
+
367
+ def test_warning_mode_high_still_exits_zero(self):
368
+ self.add_skill("bmad-warn", skill_md("Nope", "Does a thing. Use when testing."))
369
+ code, out, _ = self.run_validator(strict=False)
370
+ self.assertEqual(code, 0)
371
+ self.assertIn("Run with --strict to treat HIGH+ findings as errors.", out)
372
+
373
+ def test_no_skills_found_exits_two(self):
374
+ code, _, err = self.run_validator(strict=True)
375
+ self.assertEqual(code, 2)
376
+ self.assertEqual(err, "No skill directories found.\n")
377
+
378
+ def test_nonexistent_and_nondir_exits_two(self):
379
+ missing = str(self.root / "nope")
380
+ code, _, err = self.run_validator(skill_dir=missing)
381
+ self.assertEqual(code, 2)
382
+ self.assertEqual(err, f'Error: "{missing}" is not a valid directory.\n')
383
+
384
+ file_path = self.root / "file.txt"
385
+ file_path.write_text("x\n", encoding="utf-8")
386
+ code, _, err = self.run_validator(skill_dir=file_path)
387
+ self.assertEqual(code, 2)
388
+ self.assertEqual(err, f'Error: "{file_path}" is not a valid directory.\n')
389
+
390
+ def test_json_shape_and_severity_sort(self):
391
+ self.add_skill("bmad-json", skill_md("bmad-json", "Generates a thing with no trigger."))
392
+ self.valid("bmad-seq", {"notes.md": "ETA on this line\n"})
393
+ code, out, _ = self.run_validator(json_output=True, strict=True)
394
+ self.assertEqual(code, 0)
395
+ payload = json.loads(out)
396
+ self.assertIsInstance(payload, list)
397
+ self.assertGreaterEqual(len(payload), 2)
398
+ order = [vs.SEVERITY_ORDER[item["severity"]] for item in payload]
399
+ self.assertEqual(order, sorted(order))
400
+ required = {"skill", "rule", "title", "severity", "file", "line", "detail", "fix"}
401
+ for item in payload:
402
+ self.assertEqual(set(item), required)
403
+
404
+ def test_json_mode_skips_gha_and_step_summary(self):
405
+ skill = self.add_skill("bmad-json-gha", skill_md("Nope", "Does a thing. Use when testing."))
406
+ summary = self.root / "summary.md"
407
+ os.environ["GITHUB_ACTIONS"] = "1"
408
+ os.environ["GITHUB_STEP_SUMMARY"] = str(summary)
409
+ code, out, _ = self.run_validator(skill_dir=skill, json_output=True, strict=True)
410
+ self.assertEqual(code, 1)
411
+ self.assertNotIn("::warning", out)
412
+ self.assertFalse(summary.exists())
413
+
414
+ def test_github_actions_annotation_and_step_summary(self):
415
+ skill = self.add_skill(
416
+ "bmad-gha",
417
+ skill_md("bmad-gha", "Does a thing. Use when testing GHA."),
418
+ {"notes.md": "Ship 100% with ETA\n"},
419
+ )
420
+ summary = self.root / "summary.md"
421
+ os.environ["GITHUB_ACTIONS"] = "1"
422
+ os.environ["GITHUB_STEP_SUMMARY"] = str(summary)
423
+ code, out, _ = self.run_validator(skill_dir=skill, strict=True)
424
+ self.assertEqual(code, 0)
425
+ self.assertIn(
426
+ "::notice file=src/bmad-gha/notes.md,line=1::SEQ-02: Time estimate pattern found: \"Ship 100%25 with ETA\"",
427
+ out,
428
+ )
429
+ text = summary.read_text(encoding="utf-8")
430
+ self.assertTrue(text.startswith("## Skill Validation\n"))
431
+ self.assertIn("| Skill | Rule | Severity | File | Detail |", text)
432
+ self.assertIn("| src/bmad-gha | SEQ-02 | LOW | notes.md |", text)
433
+ self.assertIn("**1 skills scanned, 1 findings**", text)
434
+
435
+ def test_nested_skills_discovered_and_sorted(self):
436
+ self.valid("bmad-z")
437
+ nested = self.skills / "group" / "bmad-a"
438
+ write(
439
+ nested / "SKILL.md",
440
+ skill_md("bmad-a", "Nested skill. Use when testing discovery."),
441
+ )
442
+ skipped = self.skills / "node_modules" / "bmad-skip"
443
+ write(
444
+ skipped / "SKILL.md",
445
+ skill_md("bmad-skip", "Should be skipped. Use when never."),
446
+ )
447
+ code, out, _ = self.run_validator(strict=True)
448
+ self.assertEqual(code, 0)
449
+ self.assertIn("Skills scanned: 2", out)
450
+
451
+ def test_single_skill_outside_repo(self):
452
+ outside = Path(self._tmp.name) / "outside" / "bmad-out"
453
+ write(
454
+ outside / "SKILL.md",
455
+ skill_md("bmad-out", "Outside skill. Use when testing absolute paths."),
456
+ )
457
+ code, out, _ = self.run_validator(skill_dir=outside, strict=True)
458
+ self.assertEqual(code, 0)
459
+ self.assertIn("Skills scanned: 1", out)
460
+
461
+
462
+ class TestParsers(unittest.TestCase):
463
+ def test_parse_frontmatter_null_and_empty(self):
464
+ self.assertIsNone(vs.parse_frontmatter("no fence\n"))
465
+ self.assertEqual(vs.parse_frontmatter("---\n---\nbody\n"), {})
466
+ self.assertEqual(vs.parse_frontmatter("---\nname: 'quoted'\n---\n"), {"name": "quoted"})
467
+
468
+ def test_parse_frontmatter_multiline_continuation_and_comments(self):
469
+ content = "---\nname: bmad-x\ndescription: line1\n line2\n# ignored\n line3\n---\n\nBody\n"
470
+ fm = vs.parse_frontmatter_multiline(content)
471
+ self.assertEqual(fm["name"], "bmad-x")
472
+ self.assertEqual(fm["description"], "line1\n line2\n line3")
473
+
474
+
475
+ if __name__ == "__main__":
476
+ unittest.main()
@@ -1,9 +1,6 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
 
4
- const CANONICAL_LLMS_ENTRY = '**[Build]';
5
- const CANONICAL_LLMS_DESCRIPTION = 'Canonical implementation workflow for direct intent and fully planned work';
6
-
7
4
  const FORBIDDEN_TERMS = [
8
5
  /\bbmad-(?:quick-dev|dev-auto)\b/gi,
9
6
  /\bQuick[ -]?Dev\b/gi,
@@ -25,12 +22,6 @@ export function validatePublishedImplementationModel(siteDir) {
25
22
  const details = findings.map(({ file, line, match }) => `${file}:${line}: ${match}`).join('\n ');
26
23
  throw new Error(`Obsolete implementation terminology found in deployable documentation:\n ${details}`);
27
24
  }
28
-
29
- const llmsPath = path.join(siteDir, 'llms.txt');
30
- const llmsContent = fs.readFileSync(llmsPath, 'utf-8');
31
- if (!llmsContent.includes(CANONICAL_LLMS_ENTRY) || !llmsContent.includes(CANONICAL_LLMS_DESCRIPTION)) {
32
- throw new Error('llms.txt must describe Build as canonical for both direct intent and fully planned work');
33
- }
34
25
  }
35
26
 
36
27
  export function findObsoleteImplementationTerms(siteDir) {