bmad-method 6.10.1-next.2 → 6.10.1-next.21

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 (156) hide show
  1. package/.claude-plugin/marketplace.json +52 -27
  2. package/package.json +4 -3
  3. package/removals.txt +5 -0
  4. package/src/bmm-skills/1-analysis/bmad-agent-analyst/customize.toml +19 -4
  5. package/src/bmm-skills/1-analysis/bmad-product-brief/SKILL.md +1 -1
  6. package/src/bmm-skills/1-analysis/bmad-product-brief/customize.toml +5 -3
  7. package/src/bmm-skills/2-plan-workflows/bmad-prd/SKILL.md +2 -0
  8. package/src/bmm-skills/2-plan-workflows/bmad-prd/customize.toml +5 -3
  9. package/src/{core-skills → bmm-skills/2-plan-workflows}/bmad-spec/SKILL.md +18 -3
  10. package/src/{core-skills → bmm-skills/2-plan-workflows}/bmad-spec/assets/spec-template.md +1 -1
  11. package/src/bmm-skills/2-plan-workflows/bmad-spec/assets/stories-schema.md +44 -0
  12. package/src/{core-skills → bmm-skills/2-plan-workflows}/bmad-spec/customize.toml +3 -4
  13. package/src/bmm-skills/2-plan-workflows/bmad-ux/customize.toml +4 -2
  14. package/src/bmm-skills/3-solutioning/bmad-architecture/SKILL.md +2 -2
  15. package/src/bmm-skills/3-solutioning/bmad-architecture/customize.toml +4 -2
  16. package/src/bmm-skills/4-implementation/bmad-agent-dev/customize.toml +3 -0
  17. package/src/bmm-skills/4-implementation/bmad-code-review/SKILL.md +2 -4
  18. package/src/bmm-skills/4-implementation/bmad-code-review/customize.toml +52 -0
  19. package/src/bmm-skills/4-implementation/bmad-code-review/steps/step-01-gather-context.md +0 -1
  20. package/src/bmm-skills/4-implementation/bmad-code-review/steps/step-02-review.md +10 -24
  21. package/src/bmm-skills/4-implementation/bmad-code-review/steps/step-03-triage.md +3 -4
  22. package/src/bmm-skills/4-implementation/bmad-dev-auto/SKILL.md +33 -12
  23. package/src/bmm-skills/4-implementation/bmad-dev-auto/customize.toml +75 -0
  24. package/src/bmm-skills/4-implementation/bmad-dev-auto/spec-template.md +4 -4
  25. package/src/bmm-skills/4-implementation/bmad-dev-auto/step-01-clarify-and-route.md +22 -4
  26. package/src/bmm-skills/4-implementation/bmad-dev-auto/step-02-plan.md +5 -6
  27. package/src/bmm-skills/4-implementation/bmad-dev-auto/step-03-implement.md +8 -4
  28. package/src/bmm-skills/4-implementation/bmad-dev-auto/step-04-review.md +13 -19
  29. package/src/bmm-skills/4-implementation/bmad-dev-story/customize.toml +3 -0
  30. package/src/bmm-skills/4-implementation/bmad-quick-dev/SKILL.md +6 -108
  31. package/src/bmm-skills/4-implementation/bmad-quick-dev/customize.toml +73 -0
  32. package/src/bmm-skills/4-implementation/bmad-quick-dev/render.py +381 -0
  33. package/src/bmm-skills/4-implementation/bmad-quick-dev/spec-template.md +2 -2
  34. package/src/bmm-skills/4-implementation/bmad-quick-dev/step-01-clarify-and-route.md +11 -13
  35. package/src/bmm-skills/4-implementation/bmad-quick-dev/step-02-plan.md +4 -9
  36. package/src/bmm-skills/4-implementation/bmad-quick-dev/step-03-implement.md +12 -4
  37. package/src/bmm-skills/4-implementation/bmad-quick-dev/step-04-review.md +8 -20
  38. package/src/bmm-skills/4-implementation/bmad-quick-dev/step-05-present.md +4 -4
  39. package/src/bmm-skills/4-implementation/bmad-quick-dev/step-oneshot.md +12 -13
  40. package/src/bmm-skills/4-implementation/bmad-quick-dev/sync-sprint-status.md +6 -6
  41. package/src/bmm-skills/4-implementation/bmad-quick-dev/workflow.md +85 -0
  42. package/src/bmm-skills/module-help.csv +2 -4
  43. package/src/bmm-skills/module.yaml +2 -5
  44. package/src/bmm-skills/v6-shims/README.md +24 -0
  45. package/src/bmm-skills/{3-solutioning → v6-shims}/bmad-create-architecture/SKILL.md +1 -1
  46. package/src/bmm-skills/{2-plan-workflows → v6-shims}/bmad-create-prd/SKILL.md +3 -3
  47. package/src/bmm-skills/v6-shims/bmad-domain-research/SKILL.md +14 -0
  48. package/src/bmm-skills/{2-plan-workflows → v6-shims}/bmad-edit-prd/SKILL.md +3 -3
  49. package/src/bmm-skills/v6-shims/bmad-market-research/SKILL.md +14 -0
  50. package/src/bmm-skills/v6-shims/bmad-technical-research/SKILL.md +14 -0
  51. package/src/bmm-skills/{2-plan-workflows → v6-shims}/bmad-validate-prd/SKILL.md +3 -3
  52. package/src/core-skills/bmad-advanced-elicitation/SKILL.md +26 -103
  53. package/src/core-skills/bmad-advanced-elicitation/customize.toml +54 -0
  54. package/src/core-skills/bmad-advanced-elicitation/scripts/pick_methods.py +233 -0
  55. package/src/core-skills/bmad-advanced-elicitation/scripts/tests/test_pick_methods.py +228 -0
  56. package/src/core-skills/bmad-brainstorming/SKILL.md +3 -3
  57. package/src/core-skills/bmad-brainstorming/assets/brain-selector.html +2 -0
  58. package/src/core-skills/bmad-brainstorming/references/mode-autonomous.md +1 -1
  59. package/src/core-skills/bmad-brainstorming/scripts/brain.py +36 -6
  60. package/src/core-skills/bmad-brainstorming/scripts/tests/test_brain.py +22 -0
  61. package/src/core-skills/bmad-customize/SKILL.md +2 -2
  62. package/src/core-skills/bmad-deep-recon/SKILL.md +82 -0
  63. package/src/core-skills/bmad-deep-recon/assets/research.template.md +18 -0
  64. package/src/core-skills/bmad-deep-recon/customize.toml +212 -0
  65. package/src/core-skills/bmad-deep-recon/references/draft.md +8 -0
  66. package/src/core-skills/bmad-deep-recon/references/finalize.md +11 -0
  67. package/src/core-skills/bmad-deep-recon/references/html-briefing.md +16 -0
  68. package/src/core-skills/bmad-deep-recon/references/lifecycle.md +11 -0
  69. package/src/core-skills/bmad-deep-recon/references/process.md +10 -0
  70. package/src/core-skills/bmad-deep-recon/references/run.md +73 -0
  71. package/src/core-skills/bmad-deep-recon/references/selection.md +13 -0
  72. package/src/core-skills/bmad-deep-recon/references/synthesis.md +16 -0
  73. package/src/core-skills/bmad-deep-recon/references/verification.md +29 -0
  74. package/src/core-skills/bmad-deep-recon/scripts/recon_kit.py +322 -0
  75. package/src/core-skills/bmad-deep-recon/scripts/tests/test_recon_kit.py +144 -0
  76. package/src/core-skills/bmad-deep-recon/types/academic-lit.md +19 -0
  77. package/src/core-skills/bmad-deep-recon/types/competitive.md +19 -0
  78. package/src/core-skills/bmad-deep-recon/types/domain.md +19 -0
  79. package/src/core-skills/bmad-deep-recon/types/market.md +19 -0
  80. package/src/core-skills/bmad-deep-recon/types/technical.md +19 -0
  81. package/src/core-skills/bmad-deep-recon/types/user-voice.md +19 -0
  82. package/src/core-skills/bmad-forge-idea/SKILL.md +2 -2
  83. package/src/core-skills/bmad-forge-idea/scripts/resolve_personas.py +4 -1
  84. package/src/core-skills/bmad-help/SKILL.md +1 -1
  85. package/src/core-skills/bmad-party-mode/SKILL.md +2 -2
  86. package/src/core-skills/bmad-party-mode/scripts/resolve_party.py +11 -3
  87. package/src/core-skills/bmad-review/SKILL.md +49 -0
  88. package/src/core-skills/bmad-review/customize.toml +141 -0
  89. package/src/core-skills/bmad-review/references/editorial-common.md +56 -0
  90. package/src/core-skills/bmad-review/references/lens-adversarial.md +18 -0
  91. package/src/core-skills/bmad-review/references/lens-edge-case-hunter.md +54 -0
  92. package/src/core-skills/bmad-review/references/lens-prose.md +7 -0
  93. package/src/core-skills/bmad-review/references/lens-structure.md +9 -0
  94. package/src/core-skills/{bmad-review-verification-gap/SKILL.md → bmad-review/references/lens-verification-gap.md} +15 -39
  95. package/src/core-skills/bmad-review/references/structure-models.md +44 -0
  96. package/src/core-skills/bmad-review/scripts/tests/test_word_metrics.py +62 -0
  97. package/src/core-skills/bmad-review/scripts/word_metrics.py +102 -0
  98. package/src/core-skills/module-help.csv +3 -7
  99. package/src/core-skills/module.yaml +5 -0
  100. package/src/core-skills/v6-shims/README.md +25 -0
  101. package/src/core-skills/v6-shims/bmad-editorial-review/SKILL.md +6 -0
  102. package/src/core-skills/v6-shims/bmad-editorial-review/customize.toml +31 -0
  103. package/src/core-skills/v6-shims/bmad-editorial-review-prose/SKILL.md +6 -0
  104. package/src/core-skills/v6-shims/bmad-editorial-review-structure/SKILL.md +6 -0
  105. package/src/core-skills/v6-shims/bmad-review-adversarial-general/SKILL.md +6 -0
  106. package/src/core-skills/v6-shims/bmad-review-edge-case-hunter/SKILL.md +6 -0
  107. package/src/core-skills/v6-shims/bmad-review-verification-gap/SKILL.md +6 -0
  108. package/tools/installer/ide/platform-codes.yaml +7 -0
  109. package/tools/installer/modules/custom-module-manager.js +12 -6
  110. package/tools/installer/modules/external-manager.js +12 -8
  111. package/tools/installer/modules/git-env.js +47 -0
  112. package/tools/skill-validator.md +11 -1
  113. package/tools/validate-skills.js +33 -0
  114. package/src/bmm-skills/1-analysis/research/bmad-domain-research/SKILL.md +0 -96
  115. package/src/bmm-skills/1-analysis/research/bmad-domain-research/customize.toml +0 -41
  116. package/src/bmm-skills/1-analysis/research/bmad-domain-research/domain-steps/step-01-init.md +0 -137
  117. package/src/bmm-skills/1-analysis/research/bmad-domain-research/domain-steps/step-02-domain-analysis.md +0 -229
  118. package/src/bmm-skills/1-analysis/research/bmad-domain-research/domain-steps/step-03-competitive-landscape.md +0 -238
  119. package/src/bmm-skills/1-analysis/research/bmad-domain-research/domain-steps/step-04-regulatory-focus.md +0 -206
  120. package/src/bmm-skills/1-analysis/research/bmad-domain-research/domain-steps/step-05-technical-trends.md +0 -234
  121. package/src/bmm-skills/1-analysis/research/bmad-domain-research/domain-steps/step-06-research-synthesis.md +0 -450
  122. package/src/bmm-skills/1-analysis/research/bmad-domain-research/research.template.md +0 -29
  123. package/src/bmm-skills/1-analysis/research/bmad-market-research/SKILL.md +0 -96
  124. package/src/bmm-skills/1-analysis/research/bmad-market-research/customize.toml +0 -41
  125. package/src/bmm-skills/1-analysis/research/bmad-market-research/research.template.md +0 -29
  126. package/src/bmm-skills/1-analysis/research/bmad-market-research/steps/step-01-init.md +0 -184
  127. package/src/bmm-skills/1-analysis/research/bmad-market-research/steps/step-02-customer-behavior.md +0 -239
  128. package/src/bmm-skills/1-analysis/research/bmad-market-research/steps/step-03-customer-pain-points.md +0 -251
  129. package/src/bmm-skills/1-analysis/research/bmad-market-research/steps/step-04-customer-decisions.md +0 -261
  130. package/src/bmm-skills/1-analysis/research/bmad-market-research/steps/step-05-competitive-analysis.md +0 -173
  131. package/src/bmm-skills/1-analysis/research/bmad-market-research/steps/step-06-research-completion.md +0 -484
  132. package/src/bmm-skills/1-analysis/research/bmad-technical-research/SKILL.md +0 -96
  133. package/src/bmm-skills/1-analysis/research/bmad-technical-research/customize.toml +0 -41
  134. package/src/bmm-skills/1-analysis/research/bmad-technical-research/research.template.md +0 -29
  135. package/src/bmm-skills/1-analysis/research/bmad-technical-research/technical-steps/step-01-init.md +0 -137
  136. package/src/bmm-skills/1-analysis/research/bmad-technical-research/technical-steps/step-02-technical-overview.md +0 -239
  137. package/src/bmm-skills/1-analysis/research/bmad-technical-research/technical-steps/step-03-integration-patterns.md +0 -248
  138. package/src/bmm-skills/1-analysis/research/bmad-technical-research/technical-steps/step-04-architectural-patterns.md +0 -202
  139. package/src/bmm-skills/1-analysis/research/bmad-technical-research/technical-steps/step-05-implementation-research.md +0 -233
  140. package/src/bmm-skills/1-analysis/research/bmad-technical-research/technical-steps/step-06-research-synthesis.md +0 -493
  141. package/src/core-skills/bmad-brainstorming/analysis/catalog-analysis.md +0 -239
  142. package/src/core-skills/bmad-brainstorming/analysis/method-matrix.csv +0 -109
  143. package/src/core-skills/bmad-editorial-review-prose/SKILL.md +0 -86
  144. package/src/core-skills/bmad-editorial-review-structure/SKILL.md +0 -179
  145. package/src/core-skills/bmad-index-docs/SKILL.md +0 -66
  146. package/src/core-skills/bmad-review-adversarial-general/SKILL.md +0 -37
  147. package/src/core-skills/bmad-review-edge-case-hunter/SKILL.md +0 -73
  148. package/src/core-skills/bmad-review-edge-case-hunter/references/deletion-check.md +0 -14
  149. package/src/core-skills/bmad-shard-doc/SKILL.md +0 -105
  150. /package/src/{core-skills → bmm-skills/2-plan-workflows}/bmad-spec/assets/headless-schemas.md +0 -0
  151. /package/src/bmm-skills/{3-solutioning → v6-shims}/bmad-create-architecture/customize.toml +0 -0
  152. /package/src/bmm-skills/{2-plan-workflows → v6-shims}/bmad-create-prd/customize.toml +0 -0
  153. /package/src/bmm-skills/{2-plan-workflows → v6-shims}/bmad-edit-prd/customize.toml +0 -0
  154. /package/src/bmm-skills/{2-plan-workflows → v6-shims}/bmad-validate-prd/customize.toml +0 -0
  155. /package/src/core-skills/bmad-advanced-elicitation/{methods.csv → assets/methods.csv} +0 -0
  156. /package/src/core-skills/bmad-party-mode/scripts/tests/{test-resolve_party.py → test_resolve_party.py} +0 -0
@@ -0,0 +1,233 @@
1
+ #!/usr/bin/env python3
2
+ # /// script
3
+ # requires-python = ">=3.10"
4
+ # ///
5
+ """Serve the elicitation method catalog without loading it all into context.
6
+
7
+ The catalog is a CSV (num, category, method_name, description, output_pattern).
8
+ `description` is a one-line gist — enough to run the method; `output_pattern` is
9
+ a flexible flow guide (e.g. "assumptions → truths → new approach").
10
+
11
+ Commands:
12
+ categories list category names + counts (the cheap entry point)
13
+ list --category C [...] the index (num/category/name/gist) for those categories
14
+ list --all the whole catalog at once — deliberate; large, avoid interactively
15
+ show NAME_OR_NUM [...] full row for each method, matched by name or num
16
+ random [-n N] [--category C ...] [--exclude NAME ...] [--spread]
17
+ draw N at random; --spread forces category diversity
18
+ (at most one per category until categories run out) —
19
+ the reshuffle draw; --exclude skips already-shown methods
20
+
21
+ `list` refuses to run with neither --category nor --all: dumping the full catalog
22
+ into context must always be an explicit, deliberate choice.
23
+
24
+ `--extra SPEC` merges additional methods (customize.toml's `additional_methods`)
25
+ into every command. SPEC is either a JSON array literal (starts with `[`) or a
26
+ path to a JSON file; each item is {code, category, method_name, description,
27
+ output_pattern}. An extra whose method_name matches a catalog row
28
+ (case-insensitive) REPLACES it and keeps that row's num — retune a shipped
29
+ method; others append and get the next free nums, so new methods and whole new
30
+ categories are first-class and number-addressable everywhere.
31
+
32
+ Default output is lean tab-separated text for an LLM to read; --json for structured.
33
+ """
34
+ import argparse
35
+ import csv
36
+ import json
37
+ import random
38
+ import sys
39
+ from pathlib import Path
40
+
41
+ DEFAULT_FILE = Path(__file__).resolve().parent.parent / "assets" / "methods.csv"
42
+ FIELDS = ("num", "category", "method_name", "description", "output_pattern")
43
+
44
+
45
+ def load(file: Path) -> list[dict]:
46
+ # utf-8-sig: tolerate BOM-prefixed catalogs (Excel "CSV UTF-8", Notepad)
47
+ with open(file, newline="", encoding="utf-8-sig") as f:
48
+ rows = list(csv.DictReader(f))
49
+ for r in rows:
50
+ for k in FIELDS:
51
+ r.setdefault(k, "")
52
+ r[k] = (r.get(k) or "").strip()
53
+ return rows
54
+
55
+
56
+ def load_extra(spec: str) -> list[dict]:
57
+ """Parse the --extra overlay: a JSON array literal or a path to a JSON file."""
58
+ text = spec if spec.lstrip().startswith("[") else Path(spec).read_text(encoding="utf-8-sig")
59
+ data = json.loads(text)
60
+ if not isinstance(data, list):
61
+ raise ValueError("--extra must be a JSON array of objects")
62
+ rows = []
63
+ for item in data:
64
+ if not isinstance(item, dict):
65
+ raise ValueError(f"each --extra entry must be a JSON object, got: {item!r}")
66
+ row = {k: str(item.get(k) or "").strip() for k in FIELDS}
67
+ row["code"] = str(item.get("code") or "").strip() # kept for traceability
68
+ rows.append(row)
69
+ return rows
70
+
71
+
72
+ def merge_extra(rows: list[dict], extras: list[dict]) -> list[dict]:
73
+ """Extras replace a catalog row with the same method_name (case-insensitive),
74
+ otherwise append — so overrides can retune shipped methods or grow the catalog.
75
+ A replacement inherits the shipped row's num; appended extras get the next
76
+ free nums, so every merged method stays addressable by number."""
77
+ merged = list(rows)
78
+ index = {r["method_name"].lower(): i for i, r in enumerate(merged)}
79
+ for e in extras:
80
+ key = e["method_name"].lower()
81
+ if key in index:
82
+ e = dict(e)
83
+ e["num"] = e["num"] or merged[index[key]]["num"]
84
+ merged[index[key]] = e
85
+ else:
86
+ index[key] = len(merged)
87
+ merged.append(dict(e))
88
+ next_num = max((int(r["num"]) for r in merged if r["num"].isdigit()), default=0) + 1
89
+ for r in merged:
90
+ if not r["num"]:
91
+ r["num"] = str(next_num)
92
+ next_num += 1
93
+ return merged
94
+
95
+
96
+ def categories(rows: list[dict]) -> list[tuple[str, int]]:
97
+ counts: dict[str, int] = {}
98
+ for r in rows:
99
+ counts[r["category"]] = counts.get(r["category"], 0) + 1
100
+ return sorted(counts.items())
101
+
102
+
103
+ def filter_cats(rows: list[dict], cats: list[str] | None) -> list[dict]:
104
+ if not cats:
105
+ return rows
106
+ wanted = {c.lower() for c in cats}
107
+ return [r for r in rows if r["category"].lower() in wanted]
108
+
109
+
110
+ def find(rows: list[dict], names: list[str]) -> tuple[list[dict], list[str]]:
111
+ """Match each query by method_name or by num, case-insensitively."""
112
+ by_key: dict[str, dict] = {}
113
+ for r in rows:
114
+ by_key[r["method_name"].lower()] = r
115
+ if r["num"]:
116
+ by_key.setdefault(r["num"], r)
117
+ found, missing = [], []
118
+ for n in names:
119
+ r = by_key.get(n.strip().lower())
120
+ (found if r else missing).append(r if r else n)
121
+ return found, missing
122
+
123
+
124
+ def exclude(rows: list[dict], names: list[str] | None) -> list[dict]:
125
+ if not names:
126
+ return rows
127
+ skip = {n.strip().lower() for n in names}
128
+ return [r for r in rows if r["method_name"].lower() not in skip]
129
+
130
+
131
+ def spread_sample(rows: list[dict], n: int, rng: random.Random | None = None) -> list[dict]:
132
+ """Draw n methods with maximum category diversity: shuffle the categories,
133
+ take one random method per category round-robin, wrapping only when there
134
+ are fewer categories than picks."""
135
+ rng = rng or random
136
+ by_cat: dict[str, list[dict]] = {}
137
+ for r in rows:
138
+ by_cat.setdefault(r["category"], []).append(r)
139
+ buckets = list(by_cat.values())
140
+ rng.shuffle(buckets)
141
+ for b in buckets:
142
+ rng.shuffle(b)
143
+ out: list[dict] = []
144
+ while buckets and len(out) < n:
145
+ exhausted = []
146
+ for b in buckets:
147
+ if len(out) >= n:
148
+ break
149
+ out.append(b.pop())
150
+ if not b:
151
+ exhausted.append(b)
152
+ buckets = [b for b in buckets if b not in exhausted]
153
+ return out
154
+
155
+
156
+ def fmt_categories(cats: list[tuple[str, int]], as_json: bool) -> str:
157
+ if as_json:
158
+ return json.dumps([{"category": c, "count": n} for c, n in cats])
159
+ return "\n".join(f"{c}\t{n}" for c, n in cats)
160
+
161
+
162
+ def fmt_rows(rows: list[dict], as_json: bool) -> str:
163
+ if as_json:
164
+ return json.dumps([{k: r[k] for k in FIELDS} for r in rows])
165
+ return "\n".join(
166
+ f"{r['num']}\t{r['category']}\t{r['method_name']}\t{r['description']}\t{r['output_pattern']}"
167
+ for r in rows
168
+ )
169
+
170
+
171
+ def main(argv: list[str] | None = None) -> int:
172
+ if hasattr(sys.stdout, "reconfigure"):
173
+ sys.stdout.reconfigure(encoding="utf-8") # catalog rows contain →; don't die on locale code pages
174
+ p = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
175
+ p.add_argument("--file", type=Path, default=DEFAULT_FILE, help="method CSV (default: sibling assets/methods.csv)")
176
+ p.add_argument("--extra", help="additional methods: a JSON array literal or a path to a JSON file")
177
+ p.add_argument("--json", action="store_true", help="emit structured JSON instead of lean text")
178
+ sub = p.add_subparsers(dest="cmd", required=True)
179
+ sub.add_parser("categories", help="list category names + counts")
180
+ pl = sub.add_parser("list", help="the index for chosen categories (needs --category or --all)")
181
+ pl.add_argument("--category", action="append", help="filter to a category (repeatable)")
182
+ pl.add_argument("--all", action="store_true", help="dump the entire catalog (deliberate; large)")
183
+ ps = sub.add_parser("show", help="full row for each named method")
184
+ ps.add_argument("names", nargs="+", help="method names or nums")
185
+ pr = sub.add_parser("random", help="draw methods at random")
186
+ pr.add_argument("-n", type=int, default=1, help="how many (default 1)")
187
+ pr.add_argument("--category", action="append", help="restrict to a category (repeatable)")
188
+ pr.add_argument("--exclude", action="append", help="method name to skip (repeatable) — e.g. already shown")
189
+ pr.add_argument("--spread", action="store_true", help="force category diversity across the draw")
190
+ args = p.parse_args(argv)
191
+
192
+ if not args.file.is_file():
193
+ print(f"error: method file not found: {args.file}", file=sys.stderr)
194
+ return 2
195
+ rows = load(args.file)
196
+ if args.extra:
197
+ try:
198
+ rows = merge_extra(rows, load_extra(args.extra))
199
+ except (OSError, ValueError) as e:
200
+ print(f"error: could not read --extra: {e}", file=sys.stderr)
201
+ return 2
202
+
203
+ if args.cmd == "categories":
204
+ print(fmt_categories(categories(rows), args.json))
205
+ elif args.cmd == "list":
206
+ if not args.category and not args.all:
207
+ print(
208
+ "error: `list` needs --category (one or more) — or --all to dump the whole "
209
+ "catalog on purpose. Use `categories` for the cheap map, or `random` to draw blind.",
210
+ file=sys.stderr,
211
+ )
212
+ return 2
213
+ print(fmt_rows(filter_cats(rows, args.category), args.json))
214
+ elif args.cmd == "show":
215
+ found, missing = find(rows, args.names)
216
+ for m in missing:
217
+ print(f"# not found: {m}", file=sys.stderr)
218
+ if not found:
219
+ return 1
220
+ print(fmt_rows(found, args.json))
221
+ elif args.cmd == "random":
222
+ pool = exclude(filter_cats(rows, args.category), args.exclude)
223
+ if not pool:
224
+ print("# no methods match", file=sys.stderr)
225
+ return 1
226
+ n = max(0, min(args.n, len(pool))) # clamp: never crash on a negative or oversized -n
227
+ picks = spread_sample(pool, n) if args.spread else random.sample(pool, n)
228
+ print(fmt_rows(picks, args.json))
229
+ return 0
230
+
231
+
232
+ if __name__ == "__main__":
233
+ sys.exit(main())
@@ -0,0 +1,228 @@
1
+ # /// script
2
+ # requires-python = ">=3.10"
3
+ # dependencies = ["pytest>=8.0"]
4
+ # ///
5
+ """Tests for pick_methods.py.
6
+
7
+ Run: uv run scripts/tests/test_pick_methods.py
8
+ or: uv run --with pytest -m pytest scripts/tests/test_pick_methods.py
9
+ """
10
+ import json
11
+ import random
12
+ import sys
13
+ from pathlib import Path
14
+
15
+ import pytest
16
+
17
+ sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
18
+ import pick_methods # noqa: E402
19
+
20
+ CSV = """num,category,method_name,description,output_pattern
21
+ 1,risk,Pre-mortem Analysis,Imagine future failure then work backwards,failure → causes → prevention
22
+ 2,risk,Assumption Audit,List and stress-test every assumption,list → rate → stress-test
23
+ 3,core,First Principles Analysis,Rebuild from fundamental truths,assumptions → truths → new approach
24
+ 4,core,Socratic Questioning,Targeted questions reveal hidden assumptions,questions → revelations → understanding
25
+ 5,creative,SCAMPER Method,Seven creativity lenses,S→C→A→M→P→E→R
26
+ """
27
+
28
+ EXTRA = [
29
+ {
30
+ "code": "reg-inversion",
31
+ "category": "domain",
32
+ "method_name": "Regulatory Inversion",
33
+ "description": "Start from the compliance constraint",
34
+ "output_pattern": "constraint → possibility",
35
+ },
36
+ {
37
+ "code": "premortem-lite",
38
+ "category": "risk",
39
+ "method_name": "Pre-mortem Analysis",
40
+ "description": "RETUNED pre-mortem",
41
+ "output_pattern": "failure → prevention",
42
+ },
43
+ ]
44
+
45
+
46
+ @pytest.fixture
47
+ def lib(tmp_path):
48
+ csv_path = tmp_path / "methods.csv"
49
+ csv_path.write_text(CSV, encoding="utf-8")
50
+ return csv_path
51
+
52
+
53
+ def rows(lib):
54
+ return pick_methods.load(lib)
55
+
56
+
57
+ # --- load / merge -----------------------------------------------------------
58
+
59
+ def test_load_all_fields_present(lib):
60
+ r = rows(lib)
61
+ assert len(r) == 5
62
+ assert r[0]["method_name"] == "Pre-mortem Analysis"
63
+ assert all(set(pick_methods.FIELDS) <= set(row) for row in r)
64
+
65
+
66
+ def test_load_extra_json_literal_and_file(tmp_path, lib):
67
+ literal = pick_methods.load_extra(json.dumps(EXTRA))
68
+ f = tmp_path / "extra.json"
69
+ f.write_text(json.dumps(EXTRA), encoding="utf-8")
70
+ from_file = pick_methods.load_extra(str(f))
71
+ assert literal == from_file
72
+ assert literal[0]["method_name"] == "Regulatory Inversion"
73
+ assert literal[0]["num"] == "" # missing fields normalize to empty
74
+ assert literal[0]["code"] == "reg-inversion" # code survives loading
75
+
76
+
77
+ def test_merge_extra_replaces_by_name_and_appends(lib):
78
+ merged = pick_methods.merge_extra(rows(lib), pick_methods.load_extra(json.dumps(EXTRA)))
79
+ assert len(merged) == 6 # 5 shipped, 1 replaced in place, 1 appended
80
+ premortem = next(r for r in merged if r["method_name"] == "Pre-mortem Analysis")
81
+ assert premortem["description"] == "RETUNED pre-mortem"
82
+ assert premortem["num"] == "1" # replacement inherits the shipped num
83
+ appended = next(r for r in merged if r["method_name"] == "Regulatory Inversion")
84
+ assert appended["num"] == "6" # appended extras get the next free num
85
+ assert dict(pick_methods.categories(merged))["domain"] == 1 # new category is first-class
86
+
87
+
88
+ def test_extras_are_addressable_by_num(lib):
89
+ merged = pick_methods.merge_extra(rows(lib), pick_methods.load_extra(json.dumps(EXTRA)))
90
+ found, missing = pick_methods.find(merged, ["6", "1"])
91
+ assert [r["method_name"] for r in found] == ["Regulatory Inversion", "Pre-mortem Analysis"]
92
+ assert missing == []
93
+
94
+
95
+ # --- categories / filter / find / exclude -----------------------------------
96
+
97
+ def test_categories_counts_sorted(lib):
98
+ assert pick_methods.categories(rows(lib)) == [("core", 2), ("creative", 1), ("risk", 2)]
99
+
100
+
101
+ def test_filter_is_case_insensitive(lib):
102
+ got = pick_methods.filter_cats(rows(lib), ["RISK"])
103
+ assert {r["method_name"] for r in got} == {"Pre-mortem Analysis", "Assumption Audit"}
104
+
105
+
106
+ def test_filter_none_returns_all(lib):
107
+ assert len(pick_methods.filter_cats(rows(lib), None)) == 5
108
+
109
+
110
+ def test_find_by_name_num_and_missing(lib):
111
+ found, missing = pick_methods.find(rows(lib), ["scamper method", "3", "Nope"])
112
+ assert [r["method_name"] for r in found] == ["SCAMPER Method", "First Principles Analysis"]
113
+ assert missing == ["Nope"]
114
+
115
+
116
+ def test_exclude_skips_named(lib):
117
+ got = pick_methods.exclude(rows(lib), ["pre-mortem analysis", "SCAMPER Method"])
118
+ assert {r["method_name"] for r in got} == {
119
+ "Assumption Audit", "First Principles Analysis", "Socratic Questioning",
120
+ }
121
+
122
+
123
+ # --- spread sampling ---------------------------------------------------------
124
+
125
+ def test_spread_hits_distinct_categories(lib):
126
+ for seed in range(20):
127
+ picks = pick_methods.spread_sample(rows(lib), 3, random.Random(seed))
128
+ assert len({r["category"] for r in picks}) == 3
129
+
130
+
131
+ def test_spread_wraps_when_categories_run_out(lib):
132
+ picks = pick_methods.spread_sample(rows(lib), 5, random.Random(0))
133
+ assert len(picks) == 5
134
+ assert len({r["method_name"] for r in picks}) == 5 # no duplicates
135
+
136
+
137
+ def test_spread_clamps_to_pool(lib):
138
+ assert len(pick_methods.spread_sample(rows(lib), 99, random.Random(0))) == 5
139
+
140
+
141
+ # --- CLI ---------------------------------------------------------------------
142
+
143
+ def run(args, lib, capsys):
144
+ code = pick_methods.main(["--file", str(lib), *args])
145
+ captured = capsys.readouterr()
146
+ return code, captured.out, captured.err
147
+
148
+
149
+ def test_cli_categories(lib, capsys):
150
+ code, out, _ = run(["categories"], lib, capsys)
151
+ assert code == 0
152
+ assert "risk\t2" in out
153
+
154
+
155
+ def test_cli_list_requires_scope(lib, capsys):
156
+ code, _, err = run(["list"], lib, capsys)
157
+ assert code == 2
158
+ assert "--category" in err
159
+
160
+
161
+ def test_cli_list_category_and_all(lib, capsys):
162
+ code, out, _ = run(["list", "--category", "core"], lib, capsys)
163
+ assert code == 0 and len(out.strip().splitlines()) == 2
164
+ assert "Socratic Questioning" in out and "SCAMPER" not in out
165
+ code, out, _ = run(["list", "--all"], lib, capsys)
166
+ assert code == 0 and "SCAMPER" in out
167
+
168
+
169
+ def test_cli_show_found_and_missing(lib, capsys):
170
+ code, out, err = run(["show", "Assumption Audit", "Ghost"], lib, capsys)
171
+ assert code == 0
172
+ assert "stress-test" in out
173
+ assert "not found: Ghost" in err
174
+ code, _, _ = run(["show", "Ghost"], lib, capsys)
175
+ assert code == 1
176
+
177
+
178
+ def test_cli_random_spread_exclude(lib, capsys):
179
+ code, out, _ = run(
180
+ ["random", "-n", "3", "--spread", "--exclude", "SCAMPER Method"], lib, capsys
181
+ )
182
+ assert code == 0
183
+ lines = [ln for ln in out.strip().splitlines() if ln]
184
+ assert len(lines) == 3
185
+ assert "SCAMPER" not in out
186
+
187
+
188
+ def test_cli_random_clamps_and_empty_pool(lib, capsys):
189
+ code, out, _ = run(["random", "-n", "99"], lib, capsys)
190
+ assert code == 0 and len(out.strip().splitlines()) == 5
191
+ code, _, err = run(["random", "--category", "nope"], lib, capsys)
192
+ assert code == 1 and "no methods match" in err
193
+
194
+
195
+ def test_cli_extra_inline_json(lib, capsys):
196
+ code, out, _ = run(
197
+ ["--extra", json.dumps(EXTRA), "list", "--category", "domain"], lib, capsys
198
+ )
199
+ assert code == 0 and "Regulatory Inversion" in out
200
+
201
+
202
+ def test_cli_bad_extra_and_missing_file(tmp_path, lib, capsys):
203
+ code, _, err = run(["--extra", str(tmp_path / "gone.json"), "categories"], lib, capsys)
204
+ assert code == 2 and "--extra" in err
205
+ code = pick_methods.main(["--file", str(tmp_path / "gone.csv"), "categories"])
206
+ assert code == 2
207
+
208
+
209
+ def test_cli_json_output(lib, capsys):
210
+ code, out, _ = run(["--json", "show", "1"], lib, capsys)
211
+ assert code == 0
212
+ data = json.loads(out)
213
+ assert data[0]["method_name"] == "Pre-mortem Analysis"
214
+
215
+
216
+ # --- shipped catalog integration ----------------------------------------------
217
+
218
+ def test_shipped_catalog_loads_clean():
219
+ shipped = pick_methods.DEFAULT_FILE
220
+ assert shipped.is_file(), f"shipped catalog missing: {shipped}"
221
+ r = pick_methods.load(shipped)
222
+ assert len(r) >= 60
223
+ for row in r:
224
+ assert row["category"] and row["method_name"] and row["description"], row
225
+
226
+
227
+ if __name__ == "__main__":
228
+ sys.exit(pytest.main([__file__, "-q"]))
@@ -20,9 +20,9 @@ The session runs in one of three stances, chosen by the user — set explicitly
20
20
 
21
21
  1. Resolve customization: `uv run {project-root}/_bmad/scripts/resolve_customization.py --skill {skill-root} --key workflow`. On failure, use a subagent to read `{skill-root}/customize.toml` directly with defaults.
22
22
  2. Run each `{workflow.activation_steps_prepend}` entry. Treat each `{workflow.persistent_facts}` entry as foundational context (`file:`-prefixed entries are paths/globs under `{project-root}` — load their contents; others are facts verbatim).
23
- 3. Load `{project-root}/_bmad/core/config.yaml` (and `config.user.yaml` if present); resolve `{user_name}`, `{communication_language}`, `{document_output_language}`, `{output_folder}`, `{project_name}`, `{date}`. Missing → neutral defaults; never block.
24
- 4. **If launched headless** (a machine signal, not a human asking for output — `references/headless.md` lists them): load `references/headless.md` and follow it for the whole run. It is the *only* context where you generate ideas yourself; never load it otherwise.
25
- 5. **Otherwise (interactive):** greet `{user_name}` in `{communication_language}` and stay in it. Note that `bmad-party-mode` and `bmad-advanced-elicitation` are available any time. Glob `{workflow.output_dir}/*/.memlog.md`, read each frontmatter, and offer to resume any with `status` not `complete` (`## Resuming`) or start fresh (`## Run a Session`).
23
+ 3. Resolve central config: `uv run {project-root}/_bmad/scripts/resolve_config.py --project-root {project-root} --key core` (merges `_bmad/config.toml`, `_bmad/config.user.toml`, and the `_bmad/custom/` overrides); from the merged JSON resolve `{user_name}`, `{communication_language}`, `{document_output_language}`, `{output_folder}`, `{project_name}`; `{date}` is today. On failure or missing values → neutral defaults; never block.
24
+ 4. **If launched headless** (a machine signal, not a human asking for output — `references/headless.md` lists them): load `references/headless.md` and follow it for the whole run; never load it otherwise. Outside headless, you generate ideas yourself only in autonomous mode (`references/mode-autonomous.md`) — never in facilitator or partner mode.
25
+ 5. **Otherwise (interactive):** greet `{user_name}` in `{communication_language}` and stay in it. Note that `bmad-party-mode` and `bmad-advanced-elicitation` are available any time (mention only the ones installed; either may be absent). Glob `{workflow.output_dir}/*/.memlog.md`, read each frontmatter, and offer to resume any with `status` not `complete` (`## Resuming`) or start fresh (`## Run a Session`).
26
26
 
27
27
  Run each `{workflow.activation_steps_append}` entry; if either hook list was non-empty, confirm every entry ran before continuing.
28
28
 
@@ -222,6 +222,8 @@
222
222
  function checkedInvent(){ return inventBoxes.filter(function(b){ return b.checked; }); }
223
223
 
224
224
  function update(){
225
+ // rand can't exceed what the pool can supply — keep the counter honest with the draw
226
+ if (state.rand > randomPool().length){ state.rand = randomPool().length; }
225
227
  $('pickN').textContent = checkedTech().length;
226
228
  $('randN').textContent = state.rand;
227
229
  $('invN').textContent = state.inv;
@@ -2,7 +2,7 @@
2
2
 
3
3
  The user handed you the topic and wants to see what you come up with on your own, then look at the result. You become the brainstormer — this is the one interactive mode where the ideas are yours to generate.
4
4
 
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.
5
+ - **Run a real divergent session yourself.** If the user supplied techniques (e.g. a composed prompt pasted from the selector page), honor those first; otherwise pick and run techniques on your own (use `brain.py` as in `## Choosing Techniques`, but *you* choose — no menu for the user). Capture 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
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.
@@ -23,8 +23,11 @@ rather than stdout: dumping the full catalog into context is a footgun, so reach
23
23
  whole library at once must always be an explicit, deliberate choice.
24
24
 
25
25
  `--extra PATH` merges a JSON overlay of additional techniques (customize.toml's
26
- `additional_techniques`) into every command, so custom techniques and whole new
27
- categories are first-class everywhereincluding the browse page and category draws.
26
+ `additional_techniques`) into every command. An extra whose technique_name matches
27
+ a shipped row (case-insensitive) REPLACES it retune a shipped technique; others
28
+ append, so custom techniques and whole new categories are first-class everywhere —
29
+ including the browse page and category draws. (Same overlay semantics as
30
+ bmad-advanced-elicitation's pick_methods.py.)
28
31
 
29
32
  Default output is lean text for an LLM to read; pass --json for structured output.
30
33
  """
@@ -48,10 +51,11 @@ OPTIONAL_FIELDS = ("detail", "provenance", "good_for", "audience")
48
51
 
49
52
 
50
53
  def load(file: Path) -> list[dict]:
51
- with open(file, newline="", encoding="utf-8") as f:
54
+ # utf-8-sig: tolerate BOM-prefixed catalogs (Excel "CSV UTF-8", Notepad)
55
+ with open(file, newline="", encoding="utf-8-sig") as f:
52
56
  rows = list(csv.DictReader(f))
53
57
  for r in rows:
54
- for k in OPTIONAL_FIELDS:
58
+ for k in FIELDS:
55
59
  r.setdefault(k, "")
56
60
  r[k] = (r.get(k) or "").strip()
57
61
  return rows
@@ -63,9 +67,13 @@ def load_extra(file: Path) -> list[dict]:
63
67
  customize.toml's `additional_techniques` become first-class across *every*
64
68
  subcommand (categories/list/random/show/html), so the browse page and
65
69
  category draws include them too, not just the in-chat flows."""
66
- data = json.loads(file.read_text(encoding="utf-8"))
70
+ data = json.loads(file.read_text(encoding="utf-8-sig"))
71
+ if not isinstance(data, list):
72
+ raise ValueError("--extra must be a JSON array of objects")
67
73
  rows = []
68
74
  for item in data:
75
+ if not isinstance(item, dict):
76
+ raise ValueError(f"each --extra entry must be a JSON object, got: {item!r}")
69
77
  rows.append({
70
78
  "category": str(item.get("category", "")).strip(),
71
79
  "technique_name": str(item.get("technique_name", "")).strip(),
@@ -78,6 +86,22 @@ def load_extra(file: Path) -> list[dict]:
78
86
  return rows
79
87
 
80
88
 
89
+ def merge_extra(rows: list[dict], extras: list[dict]) -> list[dict]:
90
+ """Extras replace a catalog row with the same technique_name (case-insensitive),
91
+ otherwise append — the same overlay semantics as pick_methods.py, so
92
+ customize.toml additional_* entries behave identically across sibling skills."""
93
+ merged = list(rows)
94
+ index = {r["technique_name"].lower(): i for i, r in enumerate(merged)}
95
+ for e in extras:
96
+ key = e["technique_name"].lower()
97
+ if key in index:
98
+ merged[index[key]] = e
99
+ else:
100
+ index[key] = len(merged)
101
+ merged.append(e)
102
+ return merged
103
+
104
+
81
105
  def categories(rows: list[dict]) -> list[tuple[str, int]]:
82
106
  counts: dict[str, int] = {}
83
107
  for r in rows:
@@ -415,6 +439,8 @@ SELECTOR_TEMPLATE = r"""<!DOCTYPE html>
415
439
  function checkedInvent(){ return inventBoxes.filter(function(b){ return b.checked; }); }
416
440
 
417
441
  function update(){
442
+ // rand can't exceed what the pool can supply — keep the counter honest with the draw
443
+ if (state.rand > randomPool().length){ state.rand = randomPool().length; }
418
444
  $('pickN').textContent = checkedTech().length;
419
445
  $('randN').textContent = state.rand;
420
446
  $('invN').textContent = state.inv;
@@ -693,7 +719,11 @@ def main(argv: list[str] | None = None) -> int:
693
719
  if not args.extra.is_file():
694
720
  print(f"error: --extra file not found: {args.extra}", file=sys.stderr)
695
721
  return 2
696
- rows += load_extra(args.extra)
722
+ try:
723
+ rows = merge_extra(rows, load_extra(args.extra))
724
+ except (OSError, ValueError) as e:
725
+ print(f"error: could not read --extra: {e}", file=sys.stderr)
726
+ return 2
697
727
  csv_dir = args.file.resolve().parent
698
728
 
699
729
  if args.cmd == "categories":
@@ -3,6 +3,7 @@
3
3
  # dependencies = ["pytest>=8.0"]
4
4
  # ///
5
5
  """Tests for brain.py. Run: uv run -m pytest scripts/tests/test_brain.py"""
6
+ import json
6
7
  import sys
7
8
  from pathlib import Path
8
9
 
@@ -186,6 +187,27 @@ def test_extra_appears_in_list_and_random(lib, extra, capsys):
186
187
  assert "Regulatory Inversion" in capsys.readouterr().out
187
188
 
188
189
 
190
+ def test_extra_replaces_shipped_row_by_name(lib, extra, tmp_path, capsys):
191
+ shipped = brain.load(Path(lib))[0]
192
+ overlay = tmp_path / "replace.json"
193
+ overlay.write_text(
194
+ json.dumps([{"category": shipped["category"], "technique_name": shipped["technique_name"], "description": "RETUNED"}]),
195
+ encoding="utf-8",
196
+ )
197
+ brain.main(["--file", str(lib), "--extra", str(overlay), "list", "--all"])
198
+ out = capsys.readouterr().out
199
+ assert "RETUNED" in out
200
+ assert out.count(shipped["technique_name"]) == 1 # replaced, not duplicated
201
+
202
+
203
+ def test_extra_malformed_exits_cleanly(lib, tmp_path, capsys):
204
+ bad = tmp_path / "bad.json"
205
+ for content in ('{not json', '{"a": 1}', '["not-an-object"]'):
206
+ bad.write_text(content, encoding="utf-8")
207
+ assert brain.main(["--file", str(lib), "--extra", str(bad), "categories"]) == 2
208
+ assert "could not read --extra" in capsys.readouterr().err
209
+
210
+
189
211
  def test_extra_is_first_class_in_html(lib, extra, tmp_path):
190
212
  out = tmp_path / "sel.html"
191
213
  assert brain.main(["--file", str(lib), "--extra", str(extra), "html", "--out", str(out)]) == 0
@@ -31,7 +31,7 @@ Load `_bmad/config.toml` and `_bmad/config.user.toml` from `{project-root}` for
31
31
  ## Step 2: Discovery
32
32
 
33
33
  ```
34
- python3 {skill-root}/scripts/list_customizable_skills.py --project-root {project-root}
34
+ uv run {skill-root}/scripts/list_customizable_skills.py --project-root {project-root}
35
35
  ```
36
36
 
37
37
  Use `--extra-root <path>` (repeatable) if the user has skills installed in additional locations.
@@ -87,7 +87,7 @@ Default by character (policy → team, personal → user), confirm before writin
87
87
  3. Write. Create `{project-root}/_bmad/custom/` if needed.
88
88
  4. Verify:
89
89
  ```
90
- python3 {project-root}/_bmad/scripts/resolve_customization.py --skill <install-path> --key <agent-or-workflow>
90
+ uv run {project-root}/_bmad/scripts/resolve_customization.py --skill <install-path> --key <agent-or-workflow>
91
91
  ```
92
92
  Show the merged output, point out the changed fields.
93
93