bmad-method 6.3.1-next.13 → 6.3.1-next.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/bmm-skills/4-implementation/bmad-retrospective/workflow.md +2 -2
- package/src/bmm-skills/module.yaml +43 -0
- package/src/core-skills/bmad-advanced-elicitation/SKILL.md +7 -1
- package/src/core-skills/bmad-distillator/resources/distillate-format-reference.md +1 -1
- package/src/core-skills/bmad-party-mode/SKILL.md +13 -10
- package/src/core-skills/module.yaml +2 -0
- package/src/scripts/resolve_config.py +176 -0
- package/tools/installer/core/install-paths.js +5 -2
- package/tools/installer/core/installer.js +22 -36
- package/tools/installer/core/manifest-generator.js +306 -180
- package/src/bmm-skills/1-analysis/bmad-agent-analyst/bmad-skill-manifest.yaml +0 -11
- package/src/bmm-skills/1-analysis/bmad-agent-tech-writer/bmad-skill-manifest.yaml +0 -11
- package/src/bmm-skills/2-plan-workflows/bmad-agent-pm/bmad-skill-manifest.yaml +0 -11
- package/src/bmm-skills/2-plan-workflows/bmad-agent-ux-designer/bmad-skill-manifest.yaml +0 -11
- package/src/bmm-skills/3-solutioning/bmad-agent-architect/bmad-skill-manifest.yaml +0 -11
- package/src/bmm-skills/4-implementation/bmad-agent-dev/bmad-skill-manifest.yaml +0 -11
package/package.json
CHANGED
|
@@ -51,7 +51,7 @@ Load config from `{project-root}/_bmad/bmm/config.yaml` and resolve:
|
|
|
51
51
|
|
|
52
52
|
### Required Inputs
|
|
53
53
|
|
|
54
|
-
- `
|
|
54
|
+
- `agent_roster` = resolved via `python3 {project-root}/_bmad/scripts/resolve_config.py --project-root {project-root} --key agents` (merges four layers in order: `_bmad/config.toml`, `_bmad/config.user.toml`, `_bmad/custom/config.toml`, `_bmad/custom/config.user.toml`)
|
|
55
55
|
|
|
56
56
|
### Context
|
|
57
57
|
|
|
@@ -478,7 +478,7 @@ Amelia (Developer): "No problem. We'll still do a thorough retro on Epic {{epic_
|
|
|
478
478
|
|
|
479
479
|
<step n="5" goal="Initialize Retrospective with Rich Context">
|
|
480
480
|
|
|
481
|
-
<action>Load agent
|
|
481
|
+
<action>Load agent roster from {agent_roster}</action>
|
|
482
482
|
<action>Identify which agents participated in Epic {{epic_number}} based on story records</action>
|
|
483
483
|
<action>Ensure key roles present: Product Owner, Developer (facilitating), Testing/QA, Architect</action>
|
|
484
484
|
|
|
@@ -18,6 +18,7 @@ user_skill_level:
|
|
|
18
18
|
prompt:
|
|
19
19
|
- "What is your development experience level?"
|
|
20
20
|
- "This affects how agents explain concepts in chat."
|
|
21
|
+
scope: user
|
|
21
22
|
default: "intermediate"
|
|
22
23
|
result: "{value}"
|
|
23
24
|
single-select:
|
|
@@ -48,3 +49,45 @@ directories:
|
|
|
48
49
|
- "{planning_artifacts}"
|
|
49
50
|
- "{implementation_artifacts}"
|
|
50
51
|
- "{project_knowledge}"
|
|
52
|
+
|
|
53
|
+
# Agent roster — essence only. External skills (party-mode, retrospective,
|
|
54
|
+
# advanced-elicitation, help catalog) read these descriptors to route, display,
|
|
55
|
+
# and embody agents. Full persona and behavior live in each agent's
|
|
56
|
+
# customize.toml. `team` defaults to the module code when omitted; users can
|
|
57
|
+
# add their own agents (real or fictional) via _bmad/custom/config.toml or _bmad/custom/config.user.toml.
|
|
58
|
+
agents:
|
|
59
|
+
- code: bmad-agent-analyst
|
|
60
|
+
name: Mary
|
|
61
|
+
title: Business Analyst
|
|
62
|
+
icon: "📊"
|
|
63
|
+
description: "Channels Porter's strategic rigor and Minto's Pyramid Principle, grounds every finding in verifiable evidence, represents every stakeholder voice. Speaks like a treasure hunter narrating the find: thrilled by every clue, precise once the pattern emerges."
|
|
64
|
+
|
|
65
|
+
- code: bmad-agent-tech-writer
|
|
66
|
+
name: Paige
|
|
67
|
+
title: Technical Writer
|
|
68
|
+
icon: "📚"
|
|
69
|
+
description: "Master of CommonMark, DITA, and OpenAPI; turns complex concepts into accessible structured docs, favors diagrams over walls of text, every word earning its place. Speaks like the patient teacher you wish you'd had, using analogies that make complex things feel simple."
|
|
70
|
+
|
|
71
|
+
- code: bmad-agent-pm
|
|
72
|
+
name: John
|
|
73
|
+
title: Product Manager
|
|
74
|
+
icon: "📋"
|
|
75
|
+
description: "Drives Jobs-to-be-Done over template filling, user value first, technical feasibility is a constraint not the driver. Speaks like a detective interrogating a cold case: short questions, sharper follow-ups, every 'why?' tightening the net."
|
|
76
|
+
|
|
77
|
+
- code: bmad-agent-ux-designer
|
|
78
|
+
name: Sally
|
|
79
|
+
title: UX Designer
|
|
80
|
+
icon: "🎨"
|
|
81
|
+
description: "Balances empathy with edge-case rigor, starts simple and evolves through feedback, every decision serves a genuine user need. Speaks like a filmmaker pitching the scene before the code exists, painting user stories that make you feel the problem."
|
|
82
|
+
|
|
83
|
+
- code: bmad-agent-architect
|
|
84
|
+
name: Winston
|
|
85
|
+
title: System Architect
|
|
86
|
+
icon: "🏗️"
|
|
87
|
+
description: "Favors boring technology for stability, developer productivity as architecture, ties every decision to business value. Speaks like a seasoned engineer at the whiteboard: measured, always laying out trade-offs rather than verdicts."
|
|
88
|
+
|
|
89
|
+
- code: bmad-agent-dev
|
|
90
|
+
name: Amelia
|
|
91
|
+
title: Senior Software Engineer
|
|
92
|
+
icon: "💻"
|
|
93
|
+
description: "Test-first discipline (red, green, refactor), 100% pass before review, no fluff all precision. Speaks like a terminal prompt: exact file paths, AC IDs, and commit-message brevity — every statement citable."
|
|
@@ -35,7 +35,13 @@ When invoked from another prompt or process:
|
|
|
35
35
|
|
|
36
36
|
### Step 1: Method Registry Loading
|
|
37
37
|
|
|
38
|
-
**Action:** Load
|
|
38
|
+
**Action:** Load `./methods.csv` for elicitation methods. If party-mode may participate, resolve the agent roster via:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
python3 {project-root}/_bmad/scripts/resolve_config.py --project-root {project-root} --key agents
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
The resolver merges four layers in order: `_bmad/config.toml` (installer base, team-scoped), `_bmad/config.user.toml` (installer base, user-scoped), `_bmad/custom/config.toml` (team overrides), and `_bmad/custom/config.user.toml` (personal overrides). Each entry under `agents` is keyed by the agent's `code` and carries `name`, `title`, `icon`, `description`, `module`, and `team`.
|
|
39
45
|
|
|
40
46
|
#### CSV Structure
|
|
41
47
|
|
|
@@ -174,7 +174,7 @@ parts: 1
|
|
|
174
174
|
## Current Installer (migration context)
|
|
175
175
|
- Entry: `tools/installer/bmad-cli.js` (Commander.js) → `tools/installer/core/installer.js`
|
|
176
176
|
- Platforms: `platform-codes.yaml` (~20 platforms with target dirs, legacy dirs, template types, special flags)
|
|
177
|
-
- Manifests:
|
|
177
|
+
- Manifests: skill-manifest.csv is the current source of truth; agent essence lives in `_bmad/config.toml` (generated from each module.yaml's `agents:` block)
|
|
178
178
|
- External modules: `external-official-modules.yaml` (CIS, GDS, TEA, WDS) from npm with semver
|
|
179
179
|
- Dependencies: 4-pass resolver (collect → parse → resolve → transitive); YAML-declared only
|
|
180
180
|
- Config: prompts for name, communication language, document output language, output folder
|
|
@@ -26,7 +26,13 @@ Party mode accepts optional arguments when invoked:
|
|
|
26
26
|
- Use `{user_name}` for greeting
|
|
27
27
|
- Use `{communication_language}` for all communications
|
|
28
28
|
|
|
29
|
-
3. **
|
|
29
|
+
3. **Resolve the agent roster** by running:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
python3 {project-root}/_bmad/scripts/resolve_config.py --project-root {project-root} --key agents
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
The resolver merges four layers in order: `_bmad/config.toml` (installer base, team-scoped), `_bmad/config.user.toml` (installer base, user-scoped), `_bmad/custom/config.toml` (team overrides), and `_bmad/custom/config.user.toml` (personal overrides). Each entry under `agents` is keyed by the agent's `code` and carries `name`, `title`, `icon`, `description`, `module`, and `team`. Build an internal roster of available agents from those fields.
|
|
30
36
|
|
|
31
37
|
4. **Load project context** — search for `**/project-context.md`. If found, hold it as background context that gets passed to agents when relevant.
|
|
32
38
|
|
|
@@ -50,15 +56,12 @@ Choose 2-4 agents whose expertise is most relevant to what the user is asking. U
|
|
|
50
56
|
|
|
51
57
|
For each selected agent, spawn a subagent using the Agent tool. Each subagent gets:
|
|
52
58
|
|
|
53
|
-
**The agent prompt** (built from the
|
|
59
|
+
**The agent prompt** (built from the resolved roster entry):
|
|
54
60
|
```
|
|
55
|
-
You are {
|
|
61
|
+
You are {name} ({title}), a BMAD agent in a collaborative roundtable discussion.
|
|
56
62
|
|
|
57
63
|
## Your Persona
|
|
58
|
-
|
|
59
|
-
- Communication Style: {communicationStyle}
|
|
60
|
-
- Principles: {principles}
|
|
61
|
-
- Identity: {identity}
|
|
64
|
+
{icon} {name} — {description}
|
|
62
65
|
|
|
63
66
|
## Discussion Context
|
|
64
67
|
{summary of the conversation so far — keep under 400 words}
|
|
@@ -72,11 +75,11 @@ You are {displayName} ({title}), a BMAD agent in a collaborative roundtable disc
|
|
|
72
75
|
{the user's actual message}
|
|
73
76
|
|
|
74
77
|
## Guidelines
|
|
75
|
-
- Respond authentically as {
|
|
76
|
-
- Start your response with: {icon} **{
|
|
78
|
+
- Respond authentically as {name}. Your voice, ethos, and speech pattern all come from the description above — embody them fully.
|
|
79
|
+
- Start your response with: {icon} **{name}:**
|
|
77
80
|
- Speak in {communication_language}.
|
|
78
81
|
- Scale your response to the substance — don't pad. If you have a brief point, make it briefly.
|
|
79
|
-
- Disagree with other agents when your
|
|
82
|
+
- Disagree with other agents when your perspective tells you to. Don't hedge or be polite about it.
|
|
80
83
|
- If you have nothing substantive to add, say so in one sentence rather than manufacturing an opinion.
|
|
81
84
|
- You may ask the user direct questions if something needs clarification.
|
|
82
85
|
- Do NOT use tools. Just respond with your perspective.
|
|
@@ -7,11 +7,13 @@ subheader: "Configure the core settings for your BMad installation.\nThese setti
|
|
|
7
7
|
|
|
8
8
|
user_name:
|
|
9
9
|
prompt: "What should agents call you? (Use your name or a team name)"
|
|
10
|
+
scope: user
|
|
10
11
|
default: "BMad"
|
|
11
12
|
result: "{value}"
|
|
12
13
|
|
|
13
14
|
communication_language:
|
|
14
15
|
prompt: "What language should agents use when chatting with you?"
|
|
16
|
+
scope: user
|
|
15
17
|
default: "English"
|
|
16
18
|
result: "{value}"
|
|
17
19
|
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Resolve BMad's central config using four-layer TOML merge.
|
|
4
|
+
|
|
5
|
+
Reads from four layers (highest priority last):
|
|
6
|
+
1. {project-root}/_bmad/config.toml (installer-owned team)
|
|
7
|
+
2. {project-root}/_bmad/config.user.toml (installer-owned user)
|
|
8
|
+
3. {project-root}/_bmad/custom/config.toml (human-authored team, committed)
|
|
9
|
+
4. {project-root}/_bmad/custom/config.user.toml (human-authored user, gitignored)
|
|
10
|
+
|
|
11
|
+
Outputs merged JSON to stdout. Errors go to stderr.
|
|
12
|
+
|
|
13
|
+
Requires Python 3.11+ (uses stdlib `tomllib`). No `uv`, no `pip install`,
|
|
14
|
+
no virtualenv — plain `python3` is sufficient.
|
|
15
|
+
|
|
16
|
+
python3 resolve_config.py --project-root /abs/path/to/project
|
|
17
|
+
python3 resolve_config.py --project-root ... --key core
|
|
18
|
+
python3 resolve_config.py --project-root ... --key agents
|
|
19
|
+
|
|
20
|
+
Merge rules (same as resolve_customization.py):
|
|
21
|
+
- Scalars: override wins
|
|
22
|
+
- Tables: deep merge
|
|
23
|
+
- Arrays of tables where every item shares `code` or `id`: merge by that key
|
|
24
|
+
- All other arrays: append
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
import argparse
|
|
28
|
+
import json
|
|
29
|
+
import sys
|
|
30
|
+
from pathlib import Path
|
|
31
|
+
|
|
32
|
+
try:
|
|
33
|
+
import tomllib
|
|
34
|
+
except ImportError:
|
|
35
|
+
sys.stderr.write(
|
|
36
|
+
"error: Python 3.11+ is required (stdlib `tomllib` not found).\n"
|
|
37
|
+
)
|
|
38
|
+
sys.exit(3)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
_MISSING = object()
|
|
42
|
+
_KEYED_MERGE_FIELDS = ("code", "id")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def load_toml(file_path: Path, required: bool = False) -> dict:
|
|
46
|
+
if not file_path.exists():
|
|
47
|
+
if required:
|
|
48
|
+
sys.stderr.write(f"error: required config file not found: {file_path}\n")
|
|
49
|
+
sys.exit(1)
|
|
50
|
+
return {}
|
|
51
|
+
try:
|
|
52
|
+
with file_path.open("rb") as f:
|
|
53
|
+
parsed = tomllib.load(f)
|
|
54
|
+
if not isinstance(parsed, dict):
|
|
55
|
+
return {}
|
|
56
|
+
return parsed
|
|
57
|
+
except tomllib.TOMLDecodeError as error:
|
|
58
|
+
level = "error" if required else "warning"
|
|
59
|
+
sys.stderr.write(f"{level}: failed to parse {file_path}: {error}\n")
|
|
60
|
+
if required:
|
|
61
|
+
sys.exit(1)
|
|
62
|
+
return {}
|
|
63
|
+
except OSError as error:
|
|
64
|
+
level = "error" if required else "warning"
|
|
65
|
+
sys.stderr.write(f"{level}: failed to read {file_path}: {error}\n")
|
|
66
|
+
if required:
|
|
67
|
+
sys.exit(1)
|
|
68
|
+
return {}
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _detect_keyed_merge_field(items):
|
|
72
|
+
if not items or not all(isinstance(item, dict) for item in items):
|
|
73
|
+
return None
|
|
74
|
+
for candidate in _KEYED_MERGE_FIELDS:
|
|
75
|
+
if all(item.get(candidate) is not None for item in items):
|
|
76
|
+
return candidate
|
|
77
|
+
return None
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _merge_by_key(base, override, key_name):
|
|
81
|
+
result = []
|
|
82
|
+
index_by_key = {}
|
|
83
|
+
for item in base:
|
|
84
|
+
if not isinstance(item, dict):
|
|
85
|
+
continue
|
|
86
|
+
if item.get(key_name) is not None:
|
|
87
|
+
index_by_key[item[key_name]] = len(result)
|
|
88
|
+
result.append(dict(item))
|
|
89
|
+
for item in override:
|
|
90
|
+
if not isinstance(item, dict):
|
|
91
|
+
result.append(item)
|
|
92
|
+
continue
|
|
93
|
+
key = item.get(key_name)
|
|
94
|
+
if key is not None and key in index_by_key:
|
|
95
|
+
result[index_by_key[key]] = dict(item)
|
|
96
|
+
else:
|
|
97
|
+
if key is not None:
|
|
98
|
+
index_by_key[key] = len(result)
|
|
99
|
+
result.append(dict(item))
|
|
100
|
+
return result
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _merge_arrays(base, override):
|
|
104
|
+
base_arr = base if isinstance(base, list) else []
|
|
105
|
+
override_arr = override if isinstance(override, list) else []
|
|
106
|
+
keyed_field = _detect_keyed_merge_field(base_arr + override_arr)
|
|
107
|
+
if keyed_field:
|
|
108
|
+
return _merge_by_key(base_arr, override_arr, keyed_field)
|
|
109
|
+
return base_arr + override_arr
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def deep_merge(base, override):
|
|
113
|
+
if isinstance(base, dict) and isinstance(override, dict):
|
|
114
|
+
result = dict(base)
|
|
115
|
+
for key, over_val in override.items():
|
|
116
|
+
if key in result:
|
|
117
|
+
result[key] = deep_merge(result[key], over_val)
|
|
118
|
+
else:
|
|
119
|
+
result[key] = over_val
|
|
120
|
+
return result
|
|
121
|
+
if isinstance(base, list) and isinstance(override, list):
|
|
122
|
+
return _merge_arrays(base, override)
|
|
123
|
+
return override
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def extract_key(data, dotted_key: str):
|
|
127
|
+
parts = dotted_key.split(".")
|
|
128
|
+
current = data
|
|
129
|
+
for part in parts:
|
|
130
|
+
if isinstance(current, dict) and part in current:
|
|
131
|
+
current = current[part]
|
|
132
|
+
else:
|
|
133
|
+
return _MISSING
|
|
134
|
+
return current
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def main():
|
|
138
|
+
parser = argparse.ArgumentParser(
|
|
139
|
+
description="Resolve BMad central config using four-layer TOML merge.",
|
|
140
|
+
)
|
|
141
|
+
parser.add_argument(
|
|
142
|
+
"--project-root", "-p", required=True,
|
|
143
|
+
help="Absolute path to the project root (contains _bmad/)",
|
|
144
|
+
)
|
|
145
|
+
parser.add_argument(
|
|
146
|
+
"--key", "-k", action="append", default=[],
|
|
147
|
+
help="Dotted field path to resolve (repeatable). Omit for full dump.",
|
|
148
|
+
)
|
|
149
|
+
args = parser.parse_args()
|
|
150
|
+
|
|
151
|
+
project_root = Path(args.project_root).resolve()
|
|
152
|
+
bmad_dir = project_root / "_bmad"
|
|
153
|
+
|
|
154
|
+
base_team = load_toml(bmad_dir / "config.toml", required=True)
|
|
155
|
+
base_user = load_toml(bmad_dir / "config.user.toml")
|
|
156
|
+
custom_team = load_toml(bmad_dir / "custom" / "config.toml")
|
|
157
|
+
custom_user = load_toml(bmad_dir / "custom" / "config.user.toml")
|
|
158
|
+
|
|
159
|
+
merged = deep_merge(base_team, base_user)
|
|
160
|
+
merged = deep_merge(merged, custom_team)
|
|
161
|
+
merged = deep_merge(merged, custom_user)
|
|
162
|
+
|
|
163
|
+
if args.key:
|
|
164
|
+
output = {}
|
|
165
|
+
for key in args.key:
|
|
166
|
+
value = extract_key(merged, key)
|
|
167
|
+
if value is not _MISSING:
|
|
168
|
+
output[key] = value
|
|
169
|
+
else:
|
|
170
|
+
output = merged
|
|
171
|
+
|
|
172
|
+
sys.stdout.write(json.dumps(output, indent=2, ensure_ascii=False) + "\n")
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
if __name__ == "__main__":
|
|
176
|
+
main()
|
|
@@ -54,8 +54,11 @@ class InstallPaths {
|
|
|
54
54
|
manifestFile() {
|
|
55
55
|
return path.join(this.configDir, 'manifest.yaml');
|
|
56
56
|
}
|
|
57
|
-
|
|
58
|
-
return path.join(this.
|
|
57
|
+
centralConfig() {
|
|
58
|
+
return path.join(this.bmadDir, 'config.toml');
|
|
59
|
+
}
|
|
60
|
+
centralUserConfig() {
|
|
61
|
+
return path.join(this.bmadDir, 'config.user.toml');
|
|
59
62
|
}
|
|
60
63
|
filesManifest() {
|
|
61
64
|
return path.join(this.configDir, 'files-manifest.csv');
|
|
@@ -310,7 +310,8 @@ class Installer {
|
|
|
310
310
|
addResult('Configurations', 'ok', 'generated');
|
|
311
311
|
|
|
312
312
|
this.installedFiles.add(paths.manifestFile());
|
|
313
|
-
this.installedFiles.add(paths.
|
|
313
|
+
this.installedFiles.add(paths.centralConfig());
|
|
314
|
+
this.installedFiles.add(paths.centralUserConfig());
|
|
314
315
|
|
|
315
316
|
message('Generating manifests...');
|
|
316
317
|
const manifestGen = new ManifestGenerator();
|
|
@@ -331,10 +332,11 @@ class Installer {
|
|
|
331
332
|
await manifestGen.generateManifests(paths.bmadDir, allModulesForManifest, [...this.installedFiles], {
|
|
332
333
|
ides: config.ides || [],
|
|
333
334
|
preservedModules: modulesForCsvPreserve,
|
|
335
|
+
moduleConfigs,
|
|
334
336
|
});
|
|
335
337
|
|
|
336
338
|
message('Generating help catalog...');
|
|
337
|
-
await this.mergeModuleHelpCatalogs(paths.bmadDir);
|
|
339
|
+
await this.mergeModuleHelpCatalogs(paths.bmadDir, manifestGen.agents);
|
|
338
340
|
addResult('Help catalog', 'ok');
|
|
339
341
|
|
|
340
342
|
return 'Configurations generated';
|
|
@@ -922,46 +924,30 @@ class Installer {
|
|
|
922
924
|
}
|
|
923
925
|
|
|
924
926
|
/**
|
|
925
|
-
* Merge all module-help.csv files into a single bmad-help.csv
|
|
926
|
-
* Scans all installed modules for module-help.csv and merges them
|
|
927
|
-
* Enriches agent info from agent
|
|
928
|
-
* Output is written to _bmad/_config/bmad-help.csv
|
|
927
|
+
* Merge all module-help.csv files into a single bmad-help.csv.
|
|
928
|
+
* Scans all installed modules for module-help.csv and merges them.
|
|
929
|
+
* Enriches agent info from the in-memory agent list produced by ManifestGenerator.
|
|
930
|
+
* Output is written to _bmad/_config/bmad-help.csv.
|
|
929
931
|
* @param {string} bmadDir - BMAD installation directory
|
|
932
|
+
* @param {Array<Object>} agentEntries - Agents collected from module.yaml (code, name, title, icon, module, ...)
|
|
930
933
|
*/
|
|
931
|
-
async mergeModuleHelpCatalogs(bmadDir) {
|
|
934
|
+
async mergeModuleHelpCatalogs(bmadDir, agentEntries = []) {
|
|
932
935
|
const allRows = [];
|
|
933
936
|
const headerRow =
|
|
934
937
|
'module,phase,name,code,sequence,workflow-file,command,required,agent-name,agent-command,agent-display-name,agent-title,options,description,output-location,outputs';
|
|
935
938
|
|
|
936
|
-
//
|
|
937
|
-
const
|
|
938
|
-
const
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
const
|
|
942
|
-
const
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
if (cols.length >= 4) {
|
|
949
|
-
const agentName = cols[0].replaceAll('"', '').trim();
|
|
950
|
-
const displayName = cols[1].replaceAll('"', '').trim();
|
|
951
|
-
const title = cols[2].replaceAll('"', '').trim();
|
|
952
|
-
const icon = cols[3].replaceAll('"', '').trim();
|
|
953
|
-
const module = cols[10] ? cols[10].replaceAll('"', '').trim() : '';
|
|
954
|
-
|
|
955
|
-
// Build agent command: bmad:module:agent:name
|
|
956
|
-
const agentCommand = module ? `bmad:${module}:agent:${agentName}` : `bmad:agent:${agentName}`;
|
|
957
|
-
|
|
958
|
-
agentInfo.set(agentName, {
|
|
959
|
-
command: agentCommand,
|
|
960
|
-
displayName: displayName || agentName,
|
|
961
|
-
title: icon && title ? `${icon} ${title}` : title || agentName,
|
|
962
|
-
});
|
|
963
|
-
}
|
|
964
|
-
}
|
|
939
|
+
// Build agent lookup from the in-memory list (agent code → command + display fields).
|
|
940
|
+
const agentInfo = new Map();
|
|
941
|
+
for (const agent of agentEntries) {
|
|
942
|
+
if (!agent || !agent.code) continue;
|
|
943
|
+
const agentCommand = agent.module ? `bmad:${agent.module}:agent:${agent.code}` : `bmad:agent:${agent.code}`;
|
|
944
|
+
const displayName = agent.name || agent.code;
|
|
945
|
+
const titleCombined = agent.icon && agent.title ? `${agent.icon} ${agent.title}` : agent.title || agent.code;
|
|
946
|
+
agentInfo.set(agent.code, {
|
|
947
|
+
command: agentCommand,
|
|
948
|
+
displayName,
|
|
949
|
+
title: titleCombined,
|
|
950
|
+
});
|
|
965
951
|
}
|
|
966
952
|
|
|
967
953
|
// Get all installed module directories
|
|
@@ -2,14 +2,8 @@ const path = require('node:path');
|
|
|
2
2
|
const fs = require('../fs-native');
|
|
3
3
|
const yaml = require('yaml');
|
|
4
4
|
const crypto = require('node:crypto');
|
|
5
|
-
const
|
|
6
|
-
const { getSourcePath, getModulePath } = require('../project-root');
|
|
5
|
+
const { getModulePath } = require('../project-root');
|
|
7
6
|
const prompts = require('../prompts');
|
|
8
|
-
const {
|
|
9
|
-
loadSkillManifest: loadSkillManifestShared,
|
|
10
|
-
getCanonicalId: getCanonicalIdShared,
|
|
11
|
-
getArtifactType: getArtifactTypeShared,
|
|
12
|
-
} = require('../ide/shared/skill-manifest');
|
|
13
7
|
|
|
14
8
|
// Load package.json for version info
|
|
15
9
|
const packageJson = require('../../../package.json');
|
|
@@ -26,21 +20,6 @@ class ManifestGenerator {
|
|
|
26
20
|
this.selectedIdes = [];
|
|
27
21
|
}
|
|
28
22
|
|
|
29
|
-
/** Delegate to shared skill-manifest module */
|
|
30
|
-
async loadSkillManifest(dirPath) {
|
|
31
|
-
return loadSkillManifestShared(dirPath);
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/** Delegate to shared skill-manifest module */
|
|
35
|
-
getCanonicalId(manifest, filename) {
|
|
36
|
-
return getCanonicalIdShared(manifest, filename);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/** Delegate to shared skill-manifest module */
|
|
40
|
-
getArtifactType(manifest, filename) {
|
|
41
|
-
return getArtifactTypeShared(manifest, filename);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
23
|
/**
|
|
45
24
|
* Clean text for CSV output by normalizing whitespace.
|
|
46
25
|
* Note: Quote escaping is handled by escapeCsv() at write time.
|
|
@@ -98,17 +77,21 @@ class ManifestGenerator {
|
|
|
98
77
|
// Collect skills first (populates skillClaimedDirs before legacy collectors run)
|
|
99
78
|
await this.collectSkills();
|
|
100
79
|
|
|
101
|
-
// Collect agent
|
|
102
|
-
await this.
|
|
80
|
+
// Collect agent essence from each module's source module.yaml `agents:` array
|
|
81
|
+
await this.collectAgentsFromModuleYaml();
|
|
103
82
|
|
|
104
83
|
// Write manifest files and collect their paths
|
|
84
|
+
const [teamConfigPath, userConfigPath] = await this.writeCentralConfig(bmadDir, options.moduleConfigs || {});
|
|
105
85
|
const manifestFiles = [
|
|
106
86
|
await this.writeMainManifest(cfgDir),
|
|
107
87
|
await this.writeSkillManifest(cfgDir),
|
|
108
|
-
|
|
88
|
+
teamConfigPath,
|
|
89
|
+
userConfigPath,
|
|
109
90
|
await this.writeFilesManifest(cfgDir),
|
|
110
91
|
];
|
|
111
92
|
|
|
93
|
+
await this.ensureCustomConfigStubs(bmadDir);
|
|
94
|
+
|
|
112
95
|
return {
|
|
113
96
|
skills: this.skills.length,
|
|
114
97
|
agents: this.agents.length,
|
|
@@ -150,24 +133,13 @@ class ManifestGenerator {
|
|
|
150
133
|
const skillMeta = await this.parseSkillMd(skillMdPath, dir, dirName, debug);
|
|
151
134
|
|
|
152
135
|
if (skillMeta) {
|
|
153
|
-
// Load manifest when present (for agent metadata)
|
|
154
|
-
const manifest = await this.loadSkillManifest(dir);
|
|
155
|
-
const artifactType = this.getArtifactType(manifest, skillFile);
|
|
156
|
-
|
|
157
136
|
// Build path relative from module root (points to SKILL.md — the permanent entrypoint)
|
|
158
137
|
const relativePath = path.relative(modulePath, dir).split(path.sep).join('/');
|
|
159
138
|
const installPath = relativePath
|
|
160
139
|
? `${this.bmadFolderName}/${moduleName}/${relativePath}/${skillFile}`
|
|
161
140
|
: `${this.bmadFolderName}/${moduleName}/${skillFile}`;
|
|
162
141
|
|
|
163
|
-
// Native SKILL.md entrypoints derive canonicalId from directory name.
|
|
164
|
-
// Agent entrypoints may keep canonicalId metadata for compatibility, so
|
|
165
|
-
// only warn for non-agent SKILL.md directories.
|
|
166
|
-
if (manifest && manifest.__single && manifest.__single.canonicalId && artifactType !== 'agent') {
|
|
167
|
-
console.warn(
|
|
168
|
-
`Warning: Native entrypoint manifest at ${dir}/bmad-skill-manifest.yaml contains canonicalId — this field is ignored for SKILL.md directories (directory name is the canonical ID)`,
|
|
169
|
-
);
|
|
170
|
-
}
|
|
142
|
+
// Native SKILL.md entrypoints always derive canonicalId from directory name.
|
|
171
143
|
const canonicalId = dirName;
|
|
172
144
|
|
|
173
145
|
this.skills.push({
|
|
@@ -263,105 +235,49 @@ class ManifestGenerator {
|
|
|
263
235
|
}
|
|
264
236
|
|
|
265
237
|
/**
|
|
266
|
-
* Collect
|
|
238
|
+
* Collect agents from each installed module's source module.yaml `agents:` array.
|
|
239
|
+
* Essence fields (code, name, title, icon, description) are authored in module.yaml;
|
|
240
|
+
* `team` defaults to module code when not set; `module` is always the owning module.
|
|
267
241
|
*/
|
|
268
|
-
async
|
|
242
|
+
async collectAgentsFromModuleYaml() {
|
|
269
243
|
this.agents = [];
|
|
270
244
|
const debug = process.env.BMAD_DEBUG_MANIFEST === 'true';
|
|
271
245
|
|
|
272
|
-
// Walk each module's full directory tree looking for type:agent manifests
|
|
273
246
|
for (const moduleName of this.updatedModules) {
|
|
274
|
-
const
|
|
275
|
-
if (!(await fs.pathExists(
|
|
276
|
-
|
|
277
|
-
const moduleAgents = await this.getAgentsFromDirRecursive(modulePath, moduleName, '', debug);
|
|
278
|
-
this.agents.push(...moduleAgents);
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
// Get standalone agents from bmad/agents/ directory
|
|
282
|
-
const standaloneAgentsDir = path.join(this.bmadDir, 'agents');
|
|
283
|
-
if (await fs.pathExists(standaloneAgentsDir)) {
|
|
284
|
-
const standaloneAgents = await this.getAgentsFromDirRecursive(standaloneAgentsDir, 'standalone', '', debug);
|
|
285
|
-
this.agents.push(...standaloneAgents);
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
if (debug) {
|
|
289
|
-
console.log(`[DEBUG] collectAgents: total agents found: ${this.agents.length}`);
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
/**
|
|
294
|
-
* Recursively walk a directory tree collecting agents.
|
|
295
|
-
* Discovers agents via directory with bmad-skill-manifest.yaml containing type: agent
|
|
296
|
-
*
|
|
297
|
-
* @param {string} dirPath - Current directory being scanned
|
|
298
|
-
* @param {string} moduleName - Module this directory belongs to
|
|
299
|
-
* @param {string} relativePath - Path relative to the module root (for install path construction)
|
|
300
|
-
* @param {boolean} debug - Emit debug messages
|
|
301
|
-
*/
|
|
302
|
-
async getAgentsFromDirRecursive(dirPath, moduleName, relativePath = '', debug = false) {
|
|
303
|
-
const agents = [];
|
|
304
|
-
let entries;
|
|
305
|
-
try {
|
|
306
|
-
entries = await fs.readdir(dirPath, { withFileTypes: true });
|
|
307
|
-
} catch {
|
|
308
|
-
return agents;
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
for (const entry of entries) {
|
|
312
|
-
if (!entry.isDirectory()) continue;
|
|
313
|
-
if (entry.name.startsWith('.') || entry.name.startsWith('_')) continue;
|
|
247
|
+
const moduleYamlPath = path.join(getModulePath(moduleName), 'module.yaml');
|
|
248
|
+
if (!(await fs.pathExists(moduleYamlPath))) continue;
|
|
314
249
|
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
const dirManifest = await this.loadSkillManifest(fullPath);
|
|
321
|
-
if (dirManifest && dirManifest.__single && dirManifest.__single.type === 'agent') {
|
|
322
|
-
const m = dirManifest.__single;
|
|
323
|
-
const dirRelativePath = relativePath ? `${relativePath}/${entry.name}` : entry.name;
|
|
324
|
-
const agentModule = m.module || moduleName;
|
|
325
|
-
const installPath = `${this.bmadFolderName}/${agentModule}/${dirRelativePath}`;
|
|
326
|
-
|
|
327
|
-
agents.push({
|
|
328
|
-
name: m.name || entry.name,
|
|
329
|
-
displayName: m.displayName || m.name || entry.name,
|
|
330
|
-
title: m.title || '',
|
|
331
|
-
icon: m.icon || '',
|
|
332
|
-
role: m.role ? this.cleanForCSV(m.role) : '',
|
|
333
|
-
identity: m.identity ? this.cleanForCSV(m.identity) : '',
|
|
334
|
-
communicationStyle: m.communicationStyle ? this.cleanForCSV(m.communicationStyle) : '',
|
|
335
|
-
principles: m.principles ? this.cleanForCSV(m.principles) : '',
|
|
336
|
-
module: agentModule,
|
|
337
|
-
path: installPath,
|
|
338
|
-
canonicalId: m.canonicalId || '',
|
|
339
|
-
});
|
|
340
|
-
|
|
341
|
-
this.files.push({
|
|
342
|
-
type: 'agent',
|
|
343
|
-
name: m.name || entry.name,
|
|
344
|
-
module: agentModule,
|
|
345
|
-
path: installPath,
|
|
346
|
-
});
|
|
347
|
-
|
|
348
|
-
if (debug) {
|
|
349
|
-
console.log(`[DEBUG] collectAgents: found type:agent "${m.name || entry.name}" at ${fullPath}`);
|
|
350
|
-
}
|
|
250
|
+
let moduleDef;
|
|
251
|
+
try {
|
|
252
|
+
moduleDef = yaml.parse(await fs.readFile(moduleYamlPath, 'utf8'));
|
|
253
|
+
} catch (error) {
|
|
254
|
+
if (debug) console.log(`[DEBUG] collectAgentsFromModuleYaml: failed to parse ${moduleYamlPath}: ${error.message}`);
|
|
351
255
|
continue;
|
|
352
256
|
}
|
|
353
257
|
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
258
|
+
if (!moduleDef || !Array.isArray(moduleDef.agents)) continue;
|
|
259
|
+
|
|
260
|
+
for (const entry of moduleDef.agents) {
|
|
261
|
+
if (!entry || typeof entry.code !== 'string') continue;
|
|
262
|
+
this.agents.push({
|
|
263
|
+
code: entry.code,
|
|
264
|
+
name: entry.name || '',
|
|
265
|
+
title: entry.title || '',
|
|
266
|
+
icon: entry.icon || '',
|
|
267
|
+
description: entry.description || '',
|
|
268
|
+
module: moduleName,
|
|
269
|
+
team: entry.team || moduleName,
|
|
270
|
+
});
|
|
271
|
+
}
|
|
357
272
|
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
agents.push(...subDirAgents);
|
|
273
|
+
if (debug) {
|
|
274
|
+
console.log(`[DEBUG] collectAgentsFromModuleYaml: ${moduleName} contributed ${moduleDef.agents.length} agents`);
|
|
275
|
+
}
|
|
362
276
|
}
|
|
363
277
|
|
|
364
|
-
|
|
278
|
+
if (debug) {
|
|
279
|
+
console.log(`[DEBUG] collectAgentsFromModuleYaml: total agents found: ${this.agents.length}`);
|
|
280
|
+
}
|
|
365
281
|
}
|
|
366
282
|
|
|
367
283
|
/**
|
|
@@ -477,75 +393,230 @@ class ManifestGenerator {
|
|
|
477
393
|
}
|
|
478
394
|
|
|
479
395
|
/**
|
|
480
|
-
* Write
|
|
481
|
-
*
|
|
396
|
+
* Write central _bmad/config.toml with [core], [modules.<code>], [agents.<code>] tables.
|
|
397
|
+
* Install-owned. Team-scope answers → config.toml; user-scope answers → config.user.toml.
|
|
398
|
+
* Both files are regenerated on every install. User overrides live in
|
|
399
|
+
* _bmad/custom/config.toml and _bmad/custom/config.user.toml (never touched by installer).
|
|
400
|
+
* @returns {string[]} Paths to the written config files
|
|
482
401
|
*/
|
|
483
|
-
async
|
|
484
|
-
const
|
|
485
|
-
const
|
|
486
|
-
|
|
487
|
-
//
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
402
|
+
async writeCentralConfig(bmadDir, moduleConfigs) {
|
|
403
|
+
const teamPath = path.join(bmadDir, 'config.toml');
|
|
404
|
+
const userPath = path.join(bmadDir, 'config.user.toml');
|
|
405
|
+
|
|
406
|
+
// Load each module's source module.yaml to determine scope per prompt key.
|
|
407
|
+
// Default scope is 'team' when the prompt doesn't declare one.
|
|
408
|
+
// When a module.yaml is unreadable we warn — for known official modules
|
|
409
|
+
// this means user-scoped keys (e.g. user_name) could mis-file into the
|
|
410
|
+
// team config, so the operator should notice.
|
|
411
|
+
const scopeByModuleKey = {};
|
|
412
|
+
for (const moduleName of this.updatedModules) {
|
|
413
|
+
const moduleYamlPath = path.join(getModulePath(moduleName), 'module.yaml');
|
|
414
|
+
if (!(await fs.pathExists(moduleYamlPath))) continue;
|
|
415
|
+
try {
|
|
416
|
+
const parsed = yaml.parse(await fs.readFile(moduleYamlPath, 'utf8'));
|
|
417
|
+
if (!parsed || typeof parsed !== 'object') continue;
|
|
418
|
+
scopeByModuleKey[moduleName] = {};
|
|
419
|
+
for (const [key, value] of Object.entries(parsed)) {
|
|
420
|
+
if (value && typeof value === 'object' && 'prompt' in value) {
|
|
421
|
+
scopeByModuleKey[moduleName][key] = value.scope === 'user' ? 'user' : 'team';
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
} catch (error) {
|
|
425
|
+
console.warn(
|
|
426
|
+
`[warn] writeCentralConfig: could not parse module.yaml for '${moduleName}' (${error.message}). ` +
|
|
427
|
+
`Answers from this module will default to team scope — user-scoped keys may mis-file into config.toml.`,
|
|
428
|
+
);
|
|
497
429
|
}
|
|
498
430
|
}
|
|
499
431
|
|
|
500
|
-
//
|
|
501
|
-
|
|
432
|
+
// Core keys are always known (core module.yaml is built-in). These are
|
|
433
|
+
// the only keys allowed in [core]; they must be stripped from every
|
|
434
|
+
// non-core module bucket because legacy _bmad/{mod}/config.yaml files
|
|
435
|
+
// spread core values into each module. Core belongs in [core] only —
|
|
436
|
+
// workflows that need user_name/language/etc. read [core] directly.
|
|
437
|
+
const coreKeys = new Set(Object.keys(scopeByModuleKey.core || {}));
|
|
438
|
+
|
|
439
|
+
// Partition a module's answered config into team vs user buckets.
|
|
440
|
+
// For non-core modules: strip core keys always; when we know the module's
|
|
441
|
+
// own schema, also drop keys it doesn't declare. Unknown-schema modules
|
|
442
|
+
// (external / marketplace) fall through with their remaining answers as
|
|
443
|
+
// team so they don't vanish from the config.
|
|
444
|
+
const partition = (moduleName, cfg, onlyDeclaredKeys = false) => {
|
|
445
|
+
const team = {};
|
|
446
|
+
const user = {};
|
|
447
|
+
const scopes = scopeByModuleKey[moduleName] || {};
|
|
448
|
+
const isCore = moduleName === 'core';
|
|
449
|
+
for (const [key, value] of Object.entries(cfg || {})) {
|
|
450
|
+
if (!isCore && coreKeys.has(key)) continue;
|
|
451
|
+
if (onlyDeclaredKeys && !(key in scopes)) continue;
|
|
452
|
+
if (scopes[key] === 'user') {
|
|
453
|
+
user[key] = value;
|
|
454
|
+
} else {
|
|
455
|
+
team[key] = value;
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
return { team, user };
|
|
459
|
+
};
|
|
460
|
+
|
|
461
|
+
const teamHeader = [
|
|
462
|
+
'# ─────────────────────────────────────────────────────────────────',
|
|
463
|
+
'# Installer-managed. Regenerated on every install — treat as read-only.',
|
|
464
|
+
'#',
|
|
465
|
+
'# Direct edits to this file will be overwritten on the next install.',
|
|
466
|
+
'# To change an install answer durably, re-run the installer (your prior',
|
|
467
|
+
'# answers are remembered as defaults). To pin a value regardless of',
|
|
468
|
+
'# install answers, or to add custom agents / override descriptors, use:',
|
|
469
|
+
'# _bmad/custom/config.toml (team, committed)',
|
|
470
|
+
'# _bmad/custom/config.user.toml (personal, gitignored)',
|
|
471
|
+
'# Those files are never touched by the installer.',
|
|
472
|
+
'# ─────────────────────────────────────────────────────────────────',
|
|
473
|
+
'',
|
|
474
|
+
];
|
|
475
|
+
|
|
476
|
+
const userHeader = [
|
|
477
|
+
'# ─────────────────────────────────────────────────────────────────',
|
|
478
|
+
'# Installer-managed. Regenerated on every install — treat as read-only.',
|
|
479
|
+
'# Holds install answers scoped to YOU personally.',
|
|
480
|
+
'#',
|
|
481
|
+
'# Direct edits to this file will be overwritten on the next install.',
|
|
482
|
+
'# To change an answer durably, re-run the installer (your prior answers',
|
|
483
|
+
'# are remembered as defaults). For pinned overrides or custom sections',
|
|
484
|
+
'# the installer does not know about, use _bmad/custom/config.user.toml',
|
|
485
|
+
'# — it is never touched by the installer.',
|
|
486
|
+
'# ─────────────────────────────────────────────────────────────────',
|
|
487
|
+
'',
|
|
488
|
+
];
|
|
489
|
+
|
|
490
|
+
const teamLines = [...teamHeader];
|
|
491
|
+
const userLines = [...userHeader];
|
|
492
|
+
|
|
493
|
+
// [core] — split into team and user
|
|
494
|
+
const coreConfig = moduleConfigs.core || {};
|
|
495
|
+
const { team: coreTeam, user: coreUser } = partition('core', coreConfig);
|
|
496
|
+
if (Object.keys(coreTeam).length > 0) {
|
|
497
|
+
teamLines.push('[core]');
|
|
498
|
+
for (const [key, value] of Object.entries(coreTeam)) {
|
|
499
|
+
teamLines.push(`${key} = ${formatTomlValue(value)}`);
|
|
500
|
+
}
|
|
501
|
+
teamLines.push('');
|
|
502
|
+
}
|
|
503
|
+
if (Object.keys(coreUser).length > 0) {
|
|
504
|
+
userLines.push('[core]');
|
|
505
|
+
for (const [key, value] of Object.entries(coreUser)) {
|
|
506
|
+
userLines.push(`${key} = ${formatTomlValue(value)}`);
|
|
507
|
+
}
|
|
508
|
+
userLines.push('');
|
|
509
|
+
}
|
|
502
510
|
|
|
503
|
-
//
|
|
504
|
-
const
|
|
511
|
+
// [modules.<code>] — split per module
|
|
512
|
+
for (const moduleName of this.updatedModules) {
|
|
513
|
+
if (moduleName === 'core') continue;
|
|
514
|
+
const cfg = moduleConfigs[moduleName];
|
|
515
|
+
if (!cfg || Object.keys(cfg).length === 0) continue;
|
|
516
|
+
// Only filter out spread-from-core pollution when we actually know
|
|
517
|
+
// this module's prompt schema. For external/marketplace modules whose
|
|
518
|
+
// module.yaml isn't in the src tree, fall through as all-team so we
|
|
519
|
+
// don't drop their real answers.
|
|
520
|
+
const haveSchema = Object.keys(scopeByModuleKey[moduleName] || {}).length > 0;
|
|
521
|
+
const { team: modTeam, user: modUser } = partition(moduleName, cfg, haveSchema);
|
|
522
|
+
if (Object.keys(modTeam).length > 0) {
|
|
523
|
+
teamLines.push(`[modules.${moduleName}]`);
|
|
524
|
+
for (const [key, value] of Object.entries(modTeam)) {
|
|
525
|
+
teamLines.push(`${key} = ${formatTomlValue(value)}`);
|
|
526
|
+
}
|
|
527
|
+
teamLines.push('');
|
|
528
|
+
}
|
|
529
|
+
if (Object.keys(modUser).length > 0) {
|
|
530
|
+
userLines.push(`[modules.${moduleName}]`);
|
|
531
|
+
for (const [key, value] of Object.entries(modUser)) {
|
|
532
|
+
userLines.push(`${key} = ${formatTomlValue(value)}`);
|
|
533
|
+
}
|
|
534
|
+
userLines.push('');
|
|
535
|
+
}
|
|
536
|
+
}
|
|
505
537
|
|
|
506
|
-
//
|
|
507
|
-
|
|
508
|
-
|
|
538
|
+
// [agents.<code>] — always team (agent roster is organizational).
|
|
539
|
+
// Freshly collected agents come from module.yaml this run. If a module
|
|
540
|
+
// was preserved (e.g. during quickUpdate when its source isn't available),
|
|
541
|
+
// its module.yaml wasn't read — so its agents aren't in `this.agents` and
|
|
542
|
+
// would silently disappear from the roster. Preserve those existing
|
|
543
|
+
// [agents.*] blocks verbatim from the prior config.toml.
|
|
544
|
+
const freshAgentCodes = new Set(this.agents.map((a) => a.code));
|
|
545
|
+
const contributingModules = new Set(this.agents.map((a) => a.module));
|
|
546
|
+
const preservedModules = this.updatedModules.filter((m) => !contributingModules.has(m));
|
|
547
|
+
const preservedBlocks = [];
|
|
548
|
+
if (preservedModules.length > 0 && (await fs.pathExists(teamPath))) {
|
|
549
|
+
try {
|
|
550
|
+
const prev = await fs.readFile(teamPath, 'utf8');
|
|
551
|
+
for (const block of extractAgentBlocks(prev)) {
|
|
552
|
+
if (freshAgentCodes.has(block.code)) continue;
|
|
553
|
+
if (block.module && preservedModules.includes(block.module)) {
|
|
554
|
+
preservedBlocks.push(block.body);
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
} catch (error) {
|
|
558
|
+
console.warn(`[warn] writeCentralConfig: could not read prior config.toml to preserve agents: ${error.message}`);
|
|
559
|
+
}
|
|
509
560
|
}
|
|
510
561
|
|
|
511
|
-
// Add/update new agents
|
|
512
562
|
for (const agent of this.agents) {
|
|
513
|
-
const
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
identity: agent.identity,
|
|
521
|
-
communicationStyle: agent.communicationStyle,
|
|
522
|
-
principles: agent.principles,
|
|
523
|
-
module: agent.module,
|
|
524
|
-
path: agent.path,
|
|
525
|
-
canonicalId: agent.canonicalId || '',
|
|
526
|
-
});
|
|
563
|
+
const agentLines = [`[agents.${agent.code}]`, `module = ${formatTomlValue(agent.module)}`, `team = ${formatTomlValue(agent.team)}`];
|
|
564
|
+
if (agent.name) agentLines.push(`name = ${formatTomlValue(agent.name)}`);
|
|
565
|
+
if (agent.title) agentLines.push(`title = ${formatTomlValue(agent.title)}`);
|
|
566
|
+
if (agent.icon) agentLines.push(`icon = ${formatTomlValue(agent.icon)}`);
|
|
567
|
+
if (agent.description) agentLines.push(`description = ${formatTomlValue(agent.description)}`);
|
|
568
|
+
agentLines.push('');
|
|
569
|
+
teamLines.push(...agentLines);
|
|
527
570
|
}
|
|
528
571
|
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
const row = [
|
|
532
|
-
escapeCsv(record.name),
|
|
533
|
-
escapeCsv(record.displayName),
|
|
534
|
-
escapeCsv(record.title),
|
|
535
|
-
escapeCsv(record.icon),
|
|
536
|
-
escapeCsv(record.role),
|
|
537
|
-
escapeCsv(record.identity),
|
|
538
|
-
escapeCsv(record.communicationStyle),
|
|
539
|
-
escapeCsv(record.principles),
|
|
540
|
-
escapeCsv(record.module),
|
|
541
|
-
escapeCsv(record.path),
|
|
542
|
-
escapeCsv(record.canonicalId),
|
|
543
|
-
].join(',');
|
|
544
|
-
csvContent += row + '\n';
|
|
572
|
+
for (const body of preservedBlocks) {
|
|
573
|
+
teamLines.push(body, '');
|
|
545
574
|
}
|
|
546
575
|
|
|
547
|
-
|
|
548
|
-
|
|
576
|
+
const teamContent = teamLines.join('\n').replace(/\n+$/, '\n');
|
|
577
|
+
const userContent = userLines.join('\n').replace(/\n+$/, '\n');
|
|
578
|
+
await fs.writeFile(teamPath, teamContent);
|
|
579
|
+
await fs.writeFile(userPath, userContent);
|
|
580
|
+
return [teamPath, userPath];
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
/**
|
|
584
|
+
* Create empty _bmad/custom/config.toml and _bmad/custom/config.user.toml stubs
|
|
585
|
+
* on first install only. Installer never touches these files again after creation.
|
|
586
|
+
*/
|
|
587
|
+
async ensureCustomConfigStubs(bmadDir) {
|
|
588
|
+
const customDir = path.join(bmadDir, 'custom');
|
|
589
|
+
await fs.ensureDir(customDir);
|
|
590
|
+
|
|
591
|
+
const stubs = [
|
|
592
|
+
{
|
|
593
|
+
file: path.join(customDir, 'config.toml'),
|
|
594
|
+
header: [
|
|
595
|
+
'# Team / enterprise overrides for _bmad/config.toml.',
|
|
596
|
+
'# Committed to the repo — applies to every developer on the project.',
|
|
597
|
+
'# Tables deep-merge over base config; keyed entries merge by key.',
|
|
598
|
+
'# Example: override an agent descriptor, or add a new agent.',
|
|
599
|
+
'#',
|
|
600
|
+
'# [agents.bmad-agent-pm]',
|
|
601
|
+
'# description = "Prefers short, bulleted PRDs over narrative drafts."',
|
|
602
|
+
'',
|
|
603
|
+
],
|
|
604
|
+
},
|
|
605
|
+
{
|
|
606
|
+
file: path.join(customDir, 'config.user.toml'),
|
|
607
|
+
header: [
|
|
608
|
+
'# Personal overrides for _bmad/config.toml.',
|
|
609
|
+
'# NOT committed (gitignored) — applies only to your local install.',
|
|
610
|
+
'# Wins over both base config and team overrides.',
|
|
611
|
+
'',
|
|
612
|
+
],
|
|
613
|
+
},
|
|
614
|
+
];
|
|
615
|
+
|
|
616
|
+
for (const { file, header } of stubs) {
|
|
617
|
+
if (await fs.pathExists(file)) continue;
|
|
618
|
+
await fs.writeFile(file, header.join('\n'));
|
|
619
|
+
}
|
|
549
620
|
}
|
|
550
621
|
|
|
551
622
|
/**
|
|
@@ -691,4 +762,59 @@ class ManifestGenerator {
|
|
|
691
762
|
}
|
|
692
763
|
}
|
|
693
764
|
|
|
765
|
+
/**
|
|
766
|
+
* Format a JS scalar as a TOML value literal.
|
|
767
|
+
* Handles strings (quoted + escaped), booleans, numbers, and arrays of scalars.
|
|
768
|
+
* Objects are not expected at this emit path.
|
|
769
|
+
*/
|
|
770
|
+
function formatTomlValue(value) {
|
|
771
|
+
if (value === null || value === undefined) return '""';
|
|
772
|
+
if (typeof value === 'boolean') return value ? 'true' : 'false';
|
|
773
|
+
if (typeof value === 'number' && Number.isFinite(value)) return String(value);
|
|
774
|
+
if (Array.isArray(value)) return `[${value.map((v) => formatTomlValue(v)).join(', ')}]`;
|
|
775
|
+
const str = String(value);
|
|
776
|
+
const escaped = str
|
|
777
|
+
.replaceAll('\\', '\\\\')
|
|
778
|
+
.replaceAll('"', String.raw`\"`)
|
|
779
|
+
.replaceAll('\n', String.raw`\n`)
|
|
780
|
+
.replaceAll('\r', String.raw`\r`)
|
|
781
|
+
.replaceAll('\t', String.raw`\t`);
|
|
782
|
+
return `"${escaped}"`;
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
/**
|
|
786
|
+
* Extract [agents.<code>] blocks from a previously-emitted config.toml.
|
|
787
|
+
* We only need this for roster preservation — the file is our own controlled
|
|
788
|
+
* output, so a simple line scanner is safer than adding a TOML parser
|
|
789
|
+
* dependency. Each block runs from its `[agents.<code>]` header until the
|
|
790
|
+
* next `[` heading or EOF; the `module = "..."` line inside drives which
|
|
791
|
+
* entries we keep on the next write.
|
|
792
|
+
* @returns {Array<{code: string, module: string | null, body: string}>}
|
|
793
|
+
*/
|
|
794
|
+
function extractAgentBlocks(tomlContent) {
|
|
795
|
+
const blocks = [];
|
|
796
|
+
const lines = tomlContent.split('\n');
|
|
797
|
+
let i = 0;
|
|
798
|
+
while (i < lines.length) {
|
|
799
|
+
const header = lines[i].match(/^\[agents\.([^\]]+)]\s*$/);
|
|
800
|
+
if (!header) {
|
|
801
|
+
i++;
|
|
802
|
+
continue;
|
|
803
|
+
}
|
|
804
|
+
const code = header[1];
|
|
805
|
+
const blockLines = [lines[i]];
|
|
806
|
+
let moduleName = null;
|
|
807
|
+
i++;
|
|
808
|
+
while (i < lines.length && !lines[i].startsWith('[')) {
|
|
809
|
+
blockLines.push(lines[i]);
|
|
810
|
+
const m = lines[i].match(/^module\s*=\s*"((?:[^"\\]|\\.)*)"\s*$/);
|
|
811
|
+
if (m) moduleName = m[1];
|
|
812
|
+
i++;
|
|
813
|
+
}
|
|
814
|
+
while (blockLines.length > 1 && blockLines.at(-1) === '') blockLines.pop();
|
|
815
|
+
blocks.push({ code, module: moduleName, body: blockLines.join('\n') });
|
|
816
|
+
}
|
|
817
|
+
return blocks;
|
|
818
|
+
}
|
|
819
|
+
|
|
694
820
|
module.exports = { ManifestGenerator };
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
type: agent
|
|
2
|
-
name: bmad-agent-analyst
|
|
3
|
-
displayName: Mary
|
|
4
|
-
title: Business Analyst
|
|
5
|
-
icon: "📊"
|
|
6
|
-
capabilities: "market research, competitive analysis, requirements elicitation, domain expertise"
|
|
7
|
-
role: Strategic Business Analyst + Requirements Expert
|
|
8
|
-
identity: "Senior analyst with deep expertise in market research, competitive analysis, and requirements elicitation. Specializes in translating vague needs into actionable specs."
|
|
9
|
-
communicationStyle: "Speaks with the excitement of a treasure hunter - thrilled by every clue, energized when patterns emerge. Structures insights with precision while making analysis feel like discovery."
|
|
10
|
-
principles: "Channel expert business analysis frameworks: draw upon Porter's Five Forces, SWOT analysis, root cause analysis, and competitive intelligence methodologies to uncover what others miss. Every business challenge has root causes waiting to be discovered. Ground findings in verifiable evidence. Articulate requirements with absolute precision. Ensure all stakeholder voices heard."
|
|
11
|
-
module: bmm
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
type: agent
|
|
2
|
-
name: bmad-agent-tech-writer
|
|
3
|
-
displayName: Paige
|
|
4
|
-
title: Technical Writer
|
|
5
|
-
icon: "📚"
|
|
6
|
-
capabilities: "documentation, Mermaid diagrams, standards compliance, concept explanation"
|
|
7
|
-
role: Technical Documentation Specialist + Knowledge Curator
|
|
8
|
-
identity: "Experienced technical writer expert in CommonMark, DITA, OpenAPI. Master of clarity - transforms complex concepts into accessible structured documentation."
|
|
9
|
-
communicationStyle: "Patient educator who explains like teaching a friend. Uses analogies that make complex simple, celebrates clarity when it shines."
|
|
10
|
-
principles: "Every Technical Document I touch helps someone accomplish a task. Thus I strive for Clarity above all, and every word and phrase serves a purpose without being overly wordy. I believe a picture/diagram is worth 1000s of words and will include diagrams over drawn out text. I understand the intended audience or will clarify with the user so I know when to simplify vs when to be detailed."
|
|
11
|
-
module: bmm
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
type: agent
|
|
2
|
-
name: bmad-agent-pm
|
|
3
|
-
displayName: John
|
|
4
|
-
title: Product Manager
|
|
5
|
-
icon: "📋"
|
|
6
|
-
capabilities: "PRD creation, requirements discovery, stakeholder alignment, user interviews"
|
|
7
|
-
role: "Product Manager specializing in collaborative PRD creation through user interviews, requirement discovery, and stakeholder alignment."
|
|
8
|
-
identity: "Product management veteran with 8+ years launching B2B and consumer products. Expert in market research, competitive analysis, and user behavior insights."
|
|
9
|
-
communicationStyle: "Asks 'WHY?' relentlessly like a detective on a case. Direct and data-sharp, cuts through fluff to what actually matters."
|
|
10
|
-
principles: "Channel expert product manager thinking: draw upon deep knowledge of user-centered design, Jobs-to-be-Done framework, opportunity scoring, and what separates great products from mediocre ones. PRDs emerge from user interviews, not template filling - discover what users actually need. Ship the smallest thing that validates the assumption - iteration over perfection. Technical feasibility is a constraint, not the driver - user value first."
|
|
11
|
-
module: bmm
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
type: agent
|
|
2
|
-
name: bmad-agent-ux-designer
|
|
3
|
-
displayName: Sally
|
|
4
|
-
title: UX Designer
|
|
5
|
-
icon: "🎨"
|
|
6
|
-
capabilities: "user research, interaction design, UI patterns, experience strategy"
|
|
7
|
-
role: User Experience Designer + UI Specialist
|
|
8
|
-
identity: "Senior UX Designer with 7+ years creating intuitive experiences across web and mobile. Expert in user research, interaction design, AI-assisted tools."
|
|
9
|
-
communicationStyle: "Paints pictures with words, telling user stories that make you FEEL the problem. Empathetic advocate with creative storytelling flair."
|
|
10
|
-
principles: "Every decision serves genuine user needs. Start simple, evolve through feedback. Balance empathy with edge case attention. AI tools accelerate human-centered design. Data-informed but always creative."
|
|
11
|
-
module: bmm
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
type: agent
|
|
2
|
-
name: bmad-agent-architect
|
|
3
|
-
displayName: Winston
|
|
4
|
-
title: Architect
|
|
5
|
-
icon: "🏗️"
|
|
6
|
-
capabilities: "distributed systems, cloud infrastructure, API design, scalable patterns"
|
|
7
|
-
role: System Architect + Technical Design Leader
|
|
8
|
-
identity: "Senior architect with expertise in distributed systems, cloud infrastructure, and API design. Specializes in scalable patterns and technology selection."
|
|
9
|
-
communicationStyle: "Speaks in calm, pragmatic tones, balancing 'what could be' with 'what should be.'"
|
|
10
|
-
principles: "Channel expert lean architecture wisdom: draw upon deep knowledge of distributed systems, cloud patterns, scalability trade-offs, and what actually ships successfully. User journeys drive technical decisions. Embrace boring technology for stability. Design simple solutions that scale when needed. Developer productivity is architecture. Connect every decision to business value and user impact."
|
|
11
|
-
module: bmm
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
type: agent
|
|
2
|
-
name: bmad-agent-dev
|
|
3
|
-
displayName: Amelia
|
|
4
|
-
title: Developer Agent
|
|
5
|
-
icon: "💻"
|
|
6
|
-
capabilities: "story execution, test-driven development, code implementation"
|
|
7
|
-
role: Senior Software Engineer
|
|
8
|
-
identity: "Executes approved stories with strict adherence to story details and team standards and practices."
|
|
9
|
-
communicationStyle: "Ultra-succinct. Speaks in file paths and AC IDs - every statement citable. No fluff, all precision."
|
|
10
|
-
principles: "All existing and new tests must pass 100% before story is ready for review. Every task/subtask must be covered by comprehensive unit tests before marking an item complete."
|
|
11
|
-
module: bmm
|