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.
- package/.claude-plugin/marketplace.json +52 -27
- package/package.json +4 -3
- package/removals.txt +5 -0
- package/src/bmm-skills/1-analysis/bmad-agent-analyst/customize.toml +19 -4
- package/src/bmm-skills/1-analysis/bmad-product-brief/SKILL.md +1 -1
- package/src/bmm-skills/1-analysis/bmad-product-brief/customize.toml +5 -3
- package/src/bmm-skills/2-plan-workflows/bmad-prd/SKILL.md +2 -0
- package/src/bmm-skills/2-plan-workflows/bmad-prd/customize.toml +5 -3
- package/src/{core-skills → bmm-skills/2-plan-workflows}/bmad-spec/SKILL.md +18 -3
- package/src/{core-skills → bmm-skills/2-plan-workflows}/bmad-spec/assets/spec-template.md +1 -1
- package/src/bmm-skills/2-plan-workflows/bmad-spec/assets/stories-schema.md +44 -0
- package/src/{core-skills → bmm-skills/2-plan-workflows}/bmad-spec/customize.toml +3 -4
- package/src/bmm-skills/2-plan-workflows/bmad-ux/customize.toml +4 -2
- package/src/bmm-skills/3-solutioning/bmad-architecture/SKILL.md +2 -2
- package/src/bmm-skills/3-solutioning/bmad-architecture/customize.toml +4 -2
- package/src/bmm-skills/4-implementation/bmad-agent-dev/customize.toml +3 -0
- package/src/bmm-skills/4-implementation/bmad-code-review/SKILL.md +2 -4
- package/src/bmm-skills/4-implementation/bmad-code-review/customize.toml +52 -0
- package/src/bmm-skills/4-implementation/bmad-code-review/steps/step-01-gather-context.md +0 -1
- package/src/bmm-skills/4-implementation/bmad-code-review/steps/step-02-review.md +10 -24
- package/src/bmm-skills/4-implementation/bmad-code-review/steps/step-03-triage.md +3 -4
- package/src/bmm-skills/4-implementation/bmad-dev-auto/SKILL.md +33 -12
- package/src/bmm-skills/4-implementation/bmad-dev-auto/customize.toml +75 -0
- package/src/bmm-skills/4-implementation/bmad-dev-auto/spec-template.md +4 -4
- package/src/bmm-skills/4-implementation/bmad-dev-auto/step-01-clarify-and-route.md +22 -4
- package/src/bmm-skills/4-implementation/bmad-dev-auto/step-02-plan.md +5 -6
- package/src/bmm-skills/4-implementation/bmad-dev-auto/step-03-implement.md +8 -4
- package/src/bmm-skills/4-implementation/bmad-dev-auto/step-04-review.md +13 -19
- package/src/bmm-skills/4-implementation/bmad-dev-story/customize.toml +3 -0
- package/src/bmm-skills/4-implementation/bmad-quick-dev/SKILL.md +6 -108
- package/src/bmm-skills/4-implementation/bmad-quick-dev/customize.toml +73 -0
- package/src/bmm-skills/4-implementation/bmad-quick-dev/render.py +381 -0
- package/src/bmm-skills/4-implementation/bmad-quick-dev/spec-template.md +2 -2
- package/src/bmm-skills/4-implementation/bmad-quick-dev/step-01-clarify-and-route.md +11 -13
- package/src/bmm-skills/4-implementation/bmad-quick-dev/step-02-plan.md +4 -9
- package/src/bmm-skills/4-implementation/bmad-quick-dev/step-03-implement.md +12 -4
- package/src/bmm-skills/4-implementation/bmad-quick-dev/step-04-review.md +8 -20
- package/src/bmm-skills/4-implementation/bmad-quick-dev/step-05-present.md +4 -4
- package/src/bmm-skills/4-implementation/bmad-quick-dev/step-oneshot.md +12 -13
- package/src/bmm-skills/4-implementation/bmad-quick-dev/sync-sprint-status.md +6 -6
- package/src/bmm-skills/4-implementation/bmad-quick-dev/workflow.md +85 -0
- package/src/bmm-skills/module-help.csv +2 -4
- package/src/bmm-skills/module.yaml +2 -5
- package/src/bmm-skills/v6-shims/README.md +24 -0
- package/src/bmm-skills/{3-solutioning → v6-shims}/bmad-create-architecture/SKILL.md +1 -1
- package/src/bmm-skills/{2-plan-workflows → v6-shims}/bmad-create-prd/SKILL.md +3 -3
- package/src/bmm-skills/v6-shims/bmad-domain-research/SKILL.md +14 -0
- package/src/bmm-skills/{2-plan-workflows → v6-shims}/bmad-edit-prd/SKILL.md +3 -3
- package/src/bmm-skills/v6-shims/bmad-market-research/SKILL.md +14 -0
- package/src/bmm-skills/v6-shims/bmad-technical-research/SKILL.md +14 -0
- package/src/bmm-skills/{2-plan-workflows → v6-shims}/bmad-validate-prd/SKILL.md +3 -3
- package/src/core-skills/bmad-advanced-elicitation/SKILL.md +26 -103
- package/src/core-skills/bmad-advanced-elicitation/customize.toml +54 -0
- package/src/core-skills/bmad-advanced-elicitation/scripts/pick_methods.py +233 -0
- package/src/core-skills/bmad-advanced-elicitation/scripts/tests/test_pick_methods.py +228 -0
- package/src/core-skills/bmad-brainstorming/SKILL.md +3 -3
- package/src/core-skills/bmad-brainstorming/assets/brain-selector.html +2 -0
- package/src/core-skills/bmad-brainstorming/references/mode-autonomous.md +1 -1
- package/src/core-skills/bmad-brainstorming/scripts/brain.py +36 -6
- package/src/core-skills/bmad-brainstorming/scripts/tests/test_brain.py +22 -0
- package/src/core-skills/bmad-customize/SKILL.md +2 -2
- package/src/core-skills/bmad-deep-recon/SKILL.md +82 -0
- package/src/core-skills/bmad-deep-recon/assets/research.template.md +18 -0
- package/src/core-skills/bmad-deep-recon/customize.toml +212 -0
- package/src/core-skills/bmad-deep-recon/references/draft.md +8 -0
- package/src/core-skills/bmad-deep-recon/references/finalize.md +11 -0
- package/src/core-skills/bmad-deep-recon/references/html-briefing.md +16 -0
- package/src/core-skills/bmad-deep-recon/references/lifecycle.md +11 -0
- package/src/core-skills/bmad-deep-recon/references/process.md +10 -0
- package/src/core-skills/bmad-deep-recon/references/run.md +73 -0
- package/src/core-skills/bmad-deep-recon/references/selection.md +13 -0
- package/src/core-skills/bmad-deep-recon/references/synthesis.md +16 -0
- package/src/core-skills/bmad-deep-recon/references/verification.md +29 -0
- package/src/core-skills/bmad-deep-recon/scripts/recon_kit.py +322 -0
- package/src/core-skills/bmad-deep-recon/scripts/tests/test_recon_kit.py +144 -0
- package/src/core-skills/bmad-deep-recon/types/academic-lit.md +19 -0
- package/src/core-skills/bmad-deep-recon/types/competitive.md +19 -0
- package/src/core-skills/bmad-deep-recon/types/domain.md +19 -0
- package/src/core-skills/bmad-deep-recon/types/market.md +19 -0
- package/src/core-skills/bmad-deep-recon/types/technical.md +19 -0
- package/src/core-skills/bmad-deep-recon/types/user-voice.md +19 -0
- package/src/core-skills/bmad-forge-idea/SKILL.md +2 -2
- package/src/core-skills/bmad-forge-idea/scripts/resolve_personas.py +4 -1
- package/src/core-skills/bmad-help/SKILL.md +1 -1
- package/src/core-skills/bmad-party-mode/SKILL.md +2 -2
- package/src/core-skills/bmad-party-mode/scripts/resolve_party.py +11 -3
- package/src/core-skills/bmad-review/SKILL.md +49 -0
- package/src/core-skills/bmad-review/customize.toml +141 -0
- package/src/core-skills/bmad-review/references/editorial-common.md +56 -0
- package/src/core-skills/bmad-review/references/lens-adversarial.md +18 -0
- package/src/core-skills/bmad-review/references/lens-edge-case-hunter.md +54 -0
- package/src/core-skills/bmad-review/references/lens-prose.md +7 -0
- package/src/core-skills/bmad-review/references/lens-structure.md +9 -0
- package/src/core-skills/{bmad-review-verification-gap/SKILL.md → bmad-review/references/lens-verification-gap.md} +15 -39
- package/src/core-skills/bmad-review/references/structure-models.md +44 -0
- package/src/core-skills/bmad-review/scripts/tests/test_word_metrics.py +62 -0
- package/src/core-skills/bmad-review/scripts/word_metrics.py +102 -0
- package/src/core-skills/module-help.csv +3 -7
- package/src/core-skills/module.yaml +5 -0
- package/src/core-skills/v6-shims/README.md +25 -0
- package/src/core-skills/v6-shims/bmad-editorial-review/SKILL.md +6 -0
- package/src/core-skills/v6-shims/bmad-editorial-review/customize.toml +31 -0
- package/src/core-skills/v6-shims/bmad-editorial-review-prose/SKILL.md +6 -0
- package/src/core-skills/v6-shims/bmad-editorial-review-structure/SKILL.md +6 -0
- package/src/core-skills/v6-shims/bmad-review-adversarial-general/SKILL.md +6 -0
- package/src/core-skills/v6-shims/bmad-review-edge-case-hunter/SKILL.md +6 -0
- package/src/core-skills/v6-shims/bmad-review-verification-gap/SKILL.md +6 -0
- package/tools/installer/ide/platform-codes.yaml +7 -0
- package/tools/installer/modules/custom-module-manager.js +12 -6
- package/tools/installer/modules/external-manager.js +12 -8
- package/tools/installer/modules/git-env.js +47 -0
- package/tools/skill-validator.md +11 -1
- package/tools/validate-skills.js +33 -0
- package/src/bmm-skills/1-analysis/research/bmad-domain-research/SKILL.md +0 -96
- package/src/bmm-skills/1-analysis/research/bmad-domain-research/customize.toml +0 -41
- package/src/bmm-skills/1-analysis/research/bmad-domain-research/domain-steps/step-01-init.md +0 -137
- package/src/bmm-skills/1-analysis/research/bmad-domain-research/domain-steps/step-02-domain-analysis.md +0 -229
- package/src/bmm-skills/1-analysis/research/bmad-domain-research/domain-steps/step-03-competitive-landscape.md +0 -238
- package/src/bmm-skills/1-analysis/research/bmad-domain-research/domain-steps/step-04-regulatory-focus.md +0 -206
- package/src/bmm-skills/1-analysis/research/bmad-domain-research/domain-steps/step-05-technical-trends.md +0 -234
- package/src/bmm-skills/1-analysis/research/bmad-domain-research/domain-steps/step-06-research-synthesis.md +0 -450
- package/src/bmm-skills/1-analysis/research/bmad-domain-research/research.template.md +0 -29
- package/src/bmm-skills/1-analysis/research/bmad-market-research/SKILL.md +0 -96
- package/src/bmm-skills/1-analysis/research/bmad-market-research/customize.toml +0 -41
- package/src/bmm-skills/1-analysis/research/bmad-market-research/research.template.md +0 -29
- package/src/bmm-skills/1-analysis/research/bmad-market-research/steps/step-01-init.md +0 -184
- package/src/bmm-skills/1-analysis/research/bmad-market-research/steps/step-02-customer-behavior.md +0 -239
- package/src/bmm-skills/1-analysis/research/bmad-market-research/steps/step-03-customer-pain-points.md +0 -251
- package/src/bmm-skills/1-analysis/research/bmad-market-research/steps/step-04-customer-decisions.md +0 -261
- package/src/bmm-skills/1-analysis/research/bmad-market-research/steps/step-05-competitive-analysis.md +0 -173
- package/src/bmm-skills/1-analysis/research/bmad-market-research/steps/step-06-research-completion.md +0 -484
- package/src/bmm-skills/1-analysis/research/bmad-technical-research/SKILL.md +0 -96
- package/src/bmm-skills/1-analysis/research/bmad-technical-research/customize.toml +0 -41
- package/src/bmm-skills/1-analysis/research/bmad-technical-research/research.template.md +0 -29
- package/src/bmm-skills/1-analysis/research/bmad-technical-research/technical-steps/step-01-init.md +0 -137
- package/src/bmm-skills/1-analysis/research/bmad-technical-research/technical-steps/step-02-technical-overview.md +0 -239
- package/src/bmm-skills/1-analysis/research/bmad-technical-research/technical-steps/step-03-integration-patterns.md +0 -248
- package/src/bmm-skills/1-analysis/research/bmad-technical-research/technical-steps/step-04-architectural-patterns.md +0 -202
- package/src/bmm-skills/1-analysis/research/bmad-technical-research/technical-steps/step-05-implementation-research.md +0 -233
- package/src/bmm-skills/1-analysis/research/bmad-technical-research/technical-steps/step-06-research-synthesis.md +0 -493
- package/src/core-skills/bmad-brainstorming/analysis/catalog-analysis.md +0 -239
- package/src/core-skills/bmad-brainstorming/analysis/method-matrix.csv +0 -109
- package/src/core-skills/bmad-editorial-review-prose/SKILL.md +0 -86
- package/src/core-skills/bmad-editorial-review-structure/SKILL.md +0 -179
- package/src/core-skills/bmad-index-docs/SKILL.md +0 -66
- package/src/core-skills/bmad-review-adversarial-general/SKILL.md +0 -37
- package/src/core-skills/bmad-review-edge-case-hunter/SKILL.md +0 -73
- package/src/core-skills/bmad-review-edge-case-hunter/references/deletion-check.md +0 -14
- package/src/core-skills/bmad-shard-doc/SKILL.md +0 -105
- /package/src/{core-skills → bmm-skills/2-plan-workflows}/bmad-spec/assets/headless-schemas.md +0 -0
- /package/src/bmm-skills/{3-solutioning → v6-shims}/bmad-create-architecture/customize.toml +0 -0
- /package/src/bmm-skills/{2-plan-workflows → v6-shims}/bmad-create-prd/customize.toml +0 -0
- /package/src/bmm-skills/{2-plan-workflows → v6-shims}/bmad-edit-prd/customize.toml +0 -0
- /package/src/bmm-skills/{2-plan-workflows → v6-shims}/bmad-validate-prd/customize.toml +0 -0
- /package/src/core-skills/bmad-advanced-elicitation/{methods.csv → assets/methods.csv} +0 -0
- /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.
|
|
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
|
|
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.**
|
|
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
|
|
27
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|