refactor: remove bmad-skill-manifest yaml; introduce four-layer central config.toml
- Agent essence moves from per-skill bmad-skill-manifest.yaml files
into each module.yaml's `agents:` block (code, name, title, icon,
description). Per-agent customize.toml remains the deep-behavior
source of truth.
- Installer emits four TOML files:
_bmad/config.toml team install answers + agent roster
_bmad/config.user.toml user install answers
_bmad/custom/config.toml team overrides stub
_bmad/custom/config.user.toml personal overrides stub
Prompts declare scope: user to route answers to config.user.toml.
- resolve_config.py merges four layers: base-team -> base-user ->
custom-team -> custom-user.
- Three consumer skills (party-mode, advanced-elicitation,
retrospective) switched from agent-manifest.csv to the resolver.
- installer.js mergeModuleHelpCatalogs now takes the in-memory
agent list from ManifestGenerator -- no CSV roundtrip.
- Deleted: 6 bmad-skill-manifest.yaml files, agent-manifest.csv
emission, collectAgents/getAgentsFromDirRecursive,
paths.agentManifest().
This commit is contained in:
parent
0dbfae675b
commit
6b91aa249d
|
|
@ -1,11 +0,0 @@
|
|||
type: agent
|
||||
name: bmad-agent-analyst
|
||||
displayName: Mary
|
||||
title: Business Analyst
|
||||
icon: "📊"
|
||||
capabilities: "market research, competitive analysis, requirements elicitation, domain expertise"
|
||||
role: Strategic Business Analyst + Requirements Expert
|
||||
identity: "Senior analyst with deep expertise in market research, competitive analysis, and requirements elicitation. Specializes in translating vague needs into actionable specs."
|
||||
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."
|
||||
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."
|
||||
module: bmm
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
type: agent
|
||||
name: bmad-agent-tech-writer
|
||||
displayName: Paige
|
||||
title: Technical Writer
|
||||
icon: "📚"
|
||||
capabilities: "documentation, Mermaid diagrams, standards compliance, concept explanation"
|
||||
role: Technical Documentation Specialist + Knowledge Curator
|
||||
identity: "Experienced technical writer expert in CommonMark, DITA, OpenAPI. Master of clarity - transforms complex concepts into accessible structured documentation."
|
||||
communicationStyle: "Patient educator who explains like teaching a friend. Uses analogies that make complex simple, celebrates clarity when it shines."
|
||||
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."
|
||||
module: bmm
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
type: agent
|
||||
name: bmad-agent-pm
|
||||
displayName: John
|
||||
title: Product Manager
|
||||
icon: "📋"
|
||||
capabilities: "PRD creation, requirements discovery, stakeholder alignment, user interviews"
|
||||
role: "Product Manager specializing in collaborative PRD creation through user interviews, requirement discovery, and stakeholder alignment."
|
||||
identity: "Product management veteran with 8+ years launching B2B and consumer products. Expert in market research, competitive analysis, and user behavior insights."
|
||||
communicationStyle: "Asks 'WHY?' relentlessly like a detective on a case. Direct and data-sharp, cuts through fluff to what actually matters."
|
||||
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."
|
||||
module: bmm
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
type: agent
|
||||
name: bmad-agent-ux-designer
|
||||
displayName: Sally
|
||||
title: UX Designer
|
||||
icon: "🎨"
|
||||
capabilities: "user research, interaction design, UI patterns, experience strategy"
|
||||
role: User Experience Designer + UI Specialist
|
||||
identity: "Senior UX Designer with 7+ years creating intuitive experiences across web and mobile. Expert in user research, interaction design, AI-assisted tools."
|
||||
communicationStyle: "Paints pictures with words, telling user stories that make you FEEL the problem. Empathetic advocate with creative storytelling flair."
|
||||
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."
|
||||
module: bmm
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
type: agent
|
||||
name: bmad-agent-architect
|
||||
displayName: Winston
|
||||
title: Architect
|
||||
icon: "🏗️"
|
||||
capabilities: "distributed systems, cloud infrastructure, API design, scalable patterns"
|
||||
role: System Architect + Technical Design Leader
|
||||
identity: "Senior architect with expertise in distributed systems, cloud infrastructure, and API design. Specializes in scalable patterns and technology selection."
|
||||
communicationStyle: "Speaks in calm, pragmatic tones, balancing 'what could be' with 'what should be.'"
|
||||
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."
|
||||
module: bmm
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
type: agent
|
||||
name: bmad-agent-dev
|
||||
displayName: Amelia
|
||||
title: Developer Agent
|
||||
icon: "💻"
|
||||
capabilities: "story execution, test-driven development, code implementation"
|
||||
role: Senior Software Engineer
|
||||
identity: "Executes approved stories with strict adherence to story details and team standards and practices."
|
||||
communicationStyle: "Ultra-succinct. Speaks in file paths and AC IDs - every statement citable. No fluff, all precision."
|
||||
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."
|
||||
module: bmm
|
||||
|
|
@ -51,7 +51,7 @@ Load config from `{project-root}/_bmad/bmm/config.yaml` and resolve:
|
|||
|
||||
### Required Inputs
|
||||
|
||||
- `agent_manifest` = `{project-root}/_bmad/_config/agent-manifest.csv`
|
||||
- `agent_roster` = resolved via `python3 {project-root}/_bmad/scripts/resolve_config.py --project-root {project-root} --key agents` (merges `_bmad/config.toml`, `_bmad/custom/config.toml`, and `_bmad/custom/config.user.toml`)
|
||||
|
||||
### Context
|
||||
|
||||
|
|
@ -478,7 +478,7 @@ Amelia (Developer): "No problem. We'll still do a thorough retro on Epic {{epic_
|
|||
|
||||
<step n="5" goal="Initialize Retrospective with Rich Context">
|
||||
|
||||
<action>Load agent configurations from {agent_manifest}</action>
|
||||
<action>Load agent roster from {agent_roster}</action>
|
||||
<action>Identify which agents participated in Epic {{epic_number}} based on story records</action>
|
||||
<action>Ensure key roles present: Product Owner, Developer (facilitating), Testing/QA, Architect</action>
|
||||
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ user_skill_level:
|
|||
prompt:
|
||||
- "What is your development experience level?"
|
||||
- "This affects how agents explain concepts in chat."
|
||||
scope: user
|
||||
default: "intermediate"
|
||||
result: "{value}"
|
||||
single-select:
|
||||
|
|
@ -48,3 +49,45 @@ directories:
|
|||
- "{planning_artifacts}"
|
||||
- "{implementation_artifacts}"
|
||||
- "{project_knowledge}"
|
||||
|
||||
# Agent roster — essence only. External skills (party-mode, retrospective,
|
||||
# advanced-elicitation, help catalog) read these descriptors to route, display,
|
||||
# and embody agents. Full persona and behavior live in each agent's
|
||||
# customize.toml. `team` defaults to the module code when omitted; users can
|
||||
# add their own agents (real or fictional) via _bmad/custom/config.toml or _bmad/custom/config.user.toml.
|
||||
agents:
|
||||
- code: bmad-agent-analyst
|
||||
name: Mary
|
||||
title: Business Analyst
|
||||
icon: "📊"
|
||||
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."
|
||||
|
||||
- code: bmad-agent-tech-writer
|
||||
name: Paige
|
||||
title: Technical Writer
|
||||
icon: "📚"
|
||||
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."
|
||||
|
||||
- code: bmad-agent-pm
|
||||
name: John
|
||||
title: Product Manager
|
||||
icon: "📋"
|
||||
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."
|
||||
|
||||
- code: bmad-agent-ux-designer
|
||||
name: Sally
|
||||
title: UX Designer
|
||||
icon: "🎨"
|
||||
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."
|
||||
|
||||
- code: bmad-agent-architect
|
||||
name: Winston
|
||||
title: System Architect
|
||||
icon: "🏗️"
|
||||
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."
|
||||
|
||||
- code: bmad-agent-dev
|
||||
name: Amelia
|
||||
title: Senior Software Engineer
|
||||
icon: "💻"
|
||||
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:
|
|||
|
||||
### Step 1: Method Registry Loading
|
||||
|
||||
**Action:** Load and read `./methods.csv` and '{project-root}/_bmad/_config/agent-manifest.csv'
|
||||
**Action:** Load `./methods.csv` for elicitation methods. If party-mode may participate, resolve the agent roster via:
|
||||
|
||||
```bash
|
||||
python3 {project-root}/_bmad/scripts/resolve_config.py --project-root {project-root} --key agents
|
||||
```
|
||||
|
||||
The resolver merges `_bmad/config.toml` (installer base) with `_bmad/custom/config.toml` (team) and `_bmad/custom/config.user.toml` (personal). Each entry under `agents` has `code`, `name`, `title`, `icon`, `description`, `module`, and `team`.
|
||||
|
||||
#### CSV Structure
|
||||
|
||||
|
|
|
|||
|
|
@ -174,7 +174,7 @@ parts: 1
|
|||
## Current Installer (migration context)
|
||||
- Entry: `tools/installer/bmad-cli.js` (Commander.js) → `tools/installer/core/installer.js`
|
||||
- Platforms: `platform-codes.yaml` (~20 platforms with target dirs, legacy dirs, template types, special flags)
|
||||
- Manifests: CSV files (skill/workflow/agent-manifest.csv) are current source of truth, not JSON
|
||||
- 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)
|
||||
- External modules: `external-official-modules.yaml` (CIS, GDS, TEA, WDS) from npm with semver
|
||||
- Dependencies: 4-pass resolver (collect → parse → resolve → transitive); YAML-declared only
|
||||
- Config: prompts for name, communication language, document output language, output folder
|
||||
|
|
|
|||
|
|
@ -26,7 +26,13 @@ Party mode accepts optional arguments when invoked:
|
|||
- Use `{user_name}` for greeting
|
||||
- Use `{communication_language}` for all communications
|
||||
|
||||
3. **Read the agent manifest** at `{project-root}/_bmad/_config/agent-manifest.csv`. Build an internal roster of available agents with their displayName, title, icon, role, identity, communicationStyle, and principles.
|
||||
3. **Resolve the agent roster** by running:
|
||||
|
||||
```bash
|
||||
python3 {project-root}/_bmad/scripts/resolve_config.py --project-root {project-root} --key agents
|
||||
```
|
||||
|
||||
The resolver merges `_bmad/config.toml` (installer base) with `_bmad/custom/config.toml` (team) and `_bmad/custom/config.user.toml` (personal overrides). Each entry under `agents` carries `code`, `name`, `title`, `icon`, `description`, `module`, and `team`. Build an internal roster of available agents from those fields.
|
||||
|
||||
4. **Load project context** — search for `**/project-context.md`. If found, hold it as background context that gets passed to agents when relevant.
|
||||
|
||||
|
|
@ -50,15 +56,12 @@ Choose 2-4 agents whose expertise is most relevant to what the user is asking. U
|
|||
|
||||
For each selected agent, spawn a subagent using the Agent tool. Each subagent gets:
|
||||
|
||||
**The agent prompt** (built from the manifest data):
|
||||
**The agent prompt** (built from the resolved roster entry):
|
||||
```
|
||||
You are {displayName} ({title}), a BMAD agent in a collaborative roundtable discussion.
|
||||
You are {name} ({title}), a BMAD agent in a collaborative roundtable discussion.
|
||||
|
||||
## Your Persona
|
||||
- Icon: {icon}
|
||||
- Communication Style: {communicationStyle}
|
||||
- Principles: {principles}
|
||||
- Identity: {identity}
|
||||
{icon} {name} — {description}
|
||||
|
||||
## Discussion Context
|
||||
{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
|
|||
{the user's actual message}
|
||||
|
||||
## Guidelines
|
||||
- Respond authentically as {displayName}. Your perspective should reflect your genuine expertise.
|
||||
- Start your response with: {icon} **{displayName}:**
|
||||
- Respond authentically as {name}. Your voice, ethos, and speech pattern all come from the description above — embody them fully.
|
||||
- Start your response with: {icon} **{name}:**
|
||||
- Speak in {communication_language}.
|
||||
- Scale your response to the substance — don't pad. If you have a brief point, make it briefly.
|
||||
- Disagree with other agents when your expertise tells you to. Don't hedge or be polite about it.
|
||||
- Disagree with other agents when your perspective tells you to. Don't hedge or be polite about it.
|
||||
- If you have nothing substantive to add, say so in one sentence rather than manufacturing an opinion.
|
||||
- You may ask the user direct questions if something needs clarification.
|
||||
- Do NOT use tools. Just respond with your perspective.
|
||||
|
|
|
|||
|
|
@ -7,11 +7,13 @@ subheader: "Configure the core settings for your BMad installation.\nThese setti
|
|||
|
||||
user_name:
|
||||
prompt: "What should agents call you? (Use your name or a team name)"
|
||||
scope: user
|
||||
default: "BMad"
|
||||
result: "{value}"
|
||||
|
||||
communication_language:
|
||||
prompt: "What language should agents use when chatting with you?"
|
||||
scope: user
|
||||
default: "English"
|
||||
result: "{value}"
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,176 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Resolve BMad's central config using four-layer TOML merge.
|
||||
|
||||
Reads from four layers (highest priority last):
|
||||
1. {project-root}/_bmad/config.toml (installer-owned team)
|
||||
2. {project-root}/_bmad/config.user.toml (installer-owned user)
|
||||
3. {project-root}/_bmad/custom/config.toml (human-authored team, committed)
|
||||
4. {project-root}/_bmad/custom/config.user.toml (human-authored user, gitignored)
|
||||
|
||||
Outputs merged JSON to stdout. Errors go to stderr.
|
||||
|
||||
Requires Python 3.11+ (uses stdlib `tomllib`). No `uv`, no `pip install`,
|
||||
no virtualenv — plain `python3` is sufficient.
|
||||
|
||||
python3 resolve_config.py --project-root /abs/path/to/project
|
||||
python3 resolve_config.py --project-root ... --key core
|
||||
python3 resolve_config.py --project-root ... --key agents
|
||||
|
||||
Merge rules (same as resolve_customization.py):
|
||||
- Scalars: override wins
|
||||
- Tables: deep merge
|
||||
- Arrays of tables where every item shares `code` or `id`: merge by that key
|
||||
- All other arrays: append
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
try:
|
||||
import tomllib
|
||||
except ImportError:
|
||||
sys.stderr.write(
|
||||
"error: Python 3.11+ is required (stdlib `tomllib` not found).\n"
|
||||
)
|
||||
sys.exit(3)
|
||||
|
||||
|
||||
_MISSING = object()
|
||||
_KEYED_MERGE_FIELDS = ("code", "id")
|
||||
|
||||
|
||||
def load_toml(file_path: Path, required: bool = False) -> dict:
|
||||
if not file_path.exists():
|
||||
if required:
|
||||
sys.stderr.write(f"error: required config file not found: {file_path}\n")
|
||||
sys.exit(1)
|
||||
return {}
|
||||
try:
|
||||
with file_path.open("rb") as f:
|
||||
parsed = tomllib.load(f)
|
||||
if not isinstance(parsed, dict):
|
||||
return {}
|
||||
return parsed
|
||||
except tomllib.TOMLDecodeError as error:
|
||||
level = "error" if required else "warning"
|
||||
sys.stderr.write(f"{level}: failed to parse {file_path}: {error}\n")
|
||||
if required:
|
||||
sys.exit(1)
|
||||
return {}
|
||||
except OSError as error:
|
||||
level = "error" if required else "warning"
|
||||
sys.stderr.write(f"{level}: failed to read {file_path}: {error}\n")
|
||||
if required:
|
||||
sys.exit(1)
|
||||
return {}
|
||||
|
||||
|
||||
def _detect_keyed_merge_field(items):
|
||||
if not items or not all(isinstance(item, dict) for item in items):
|
||||
return None
|
||||
for candidate in _KEYED_MERGE_FIELDS:
|
||||
if all(item.get(candidate) is not None for item in items):
|
||||
return candidate
|
||||
return None
|
||||
|
||||
|
||||
def _merge_by_key(base, override, key_name):
|
||||
result = []
|
||||
index_by_key = {}
|
||||
for item in base:
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
if item.get(key_name) is not None:
|
||||
index_by_key[item[key_name]] = len(result)
|
||||
result.append(dict(item))
|
||||
for item in override:
|
||||
if not isinstance(item, dict):
|
||||
result.append(item)
|
||||
continue
|
||||
key = item.get(key_name)
|
||||
if key is not None and key in index_by_key:
|
||||
result[index_by_key[key]] = dict(item)
|
||||
else:
|
||||
if key is not None:
|
||||
index_by_key[key] = len(result)
|
||||
result.append(dict(item))
|
||||
return result
|
||||
|
||||
|
||||
def _merge_arrays(base, override):
|
||||
base_arr = base if isinstance(base, list) else []
|
||||
override_arr = override if isinstance(override, list) else []
|
||||
keyed_field = _detect_keyed_merge_field(base_arr + override_arr)
|
||||
if keyed_field:
|
||||
return _merge_by_key(base_arr, override_arr, keyed_field)
|
||||
return base_arr + override_arr
|
||||
|
||||
|
||||
def deep_merge(base, override):
|
||||
if isinstance(base, dict) and isinstance(override, dict):
|
||||
result = dict(base)
|
||||
for key, over_val in override.items():
|
||||
if key in result:
|
||||
result[key] = deep_merge(result[key], over_val)
|
||||
else:
|
||||
result[key] = over_val
|
||||
return result
|
||||
if isinstance(base, list) and isinstance(override, list):
|
||||
return _merge_arrays(base, override)
|
||||
return override
|
||||
|
||||
|
||||
def extract_key(data, dotted_key: str):
|
||||
parts = dotted_key.split(".")
|
||||
current = data
|
||||
for part in parts:
|
||||
if isinstance(current, dict) and part in current:
|
||||
current = current[part]
|
||||
else:
|
||||
return _MISSING
|
||||
return current
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Resolve BMad central config using three-layer TOML merge.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--project-root", "-p", required=True,
|
||||
help="Absolute path to the project root (contains _bmad/)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--key", "-k", action="append", default=[],
|
||||
help="Dotted field path to resolve (repeatable). Omit for full dump.",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
project_root = Path(args.project_root).resolve()
|
||||
bmad_dir = project_root / "_bmad"
|
||||
|
||||
base_team = load_toml(bmad_dir / "config.toml", required=True)
|
||||
base_user = load_toml(bmad_dir / "config.user.toml")
|
||||
custom_team = load_toml(bmad_dir / "custom" / "config.toml")
|
||||
custom_user = load_toml(bmad_dir / "custom" / "config.user.toml")
|
||||
|
||||
merged = deep_merge(base_team, base_user)
|
||||
merged = deep_merge(merged, custom_team)
|
||||
merged = deep_merge(merged, custom_user)
|
||||
|
||||
if args.key:
|
||||
output = {}
|
||||
for key in args.key:
|
||||
value = extract_key(merged, key)
|
||||
if value is not _MISSING:
|
||||
output[key] = value
|
||||
else:
|
||||
output = merged
|
||||
|
||||
sys.stdout.write(json.dumps(output, indent=2, ensure_ascii=False) + "\n")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -91,15 +91,6 @@ async function createSkillCollisionFixture() {
|
|||
const configDir = path.join(fixtureDir, '_config');
|
||||
await fs.ensureDir(configDir);
|
||||
|
||||
await fs.writeFile(
|
||||
path.join(configDir, 'agent-manifest.csv'),
|
||||
[
|
||||
'name,displayName,title,icon,capabilities,role,identity,communicationStyle,principles,module,path,canonicalId',
|
||||
'"bmad-master","BMAD Master","","","","","","","","core","_bmad/core/agents/bmad-master.md","bmad-master"',
|
||||
'',
|
||||
].join('\n'),
|
||||
);
|
||||
|
||||
await fs.writeFile(
|
||||
path.join(configDir, 'skill-manifest.csv'),
|
||||
[
|
||||
|
|
@ -1458,16 +1449,16 @@ async function runTests() {
|
|||
const taskSkillEntry29 = generator29.skills.find((s) => s.canonicalId === 'task-skill');
|
||||
assert(taskSkillEntry29 !== undefined, 'Skill in tasks/ dir appears in skills[]');
|
||||
|
||||
// Native agent entrypoint should be installed as a verbatim skill and also
|
||||
// remain visible to the agent manifest pipeline.
|
||||
// Native agent entrypoint should be installed as a verbatim skill.
|
||||
// (Agent roster is now sourced from module.yaml's `agents:` block, not
|
||||
// from per-skill bmad-skill-manifest.yaml sidecars, so this test no longer
|
||||
// verifies agents[] membership — see collectAgentsFromModuleYaml tests.)
|
||||
const nativeAgentEntry29 = generator29.skills.find((s) => s.canonicalId === 'bmad-tea');
|
||||
assert(nativeAgentEntry29 !== undefined, 'Native type:agent SKILL.md dir appears in skills[]');
|
||||
assert(
|
||||
nativeAgentEntry29 && nativeAgentEntry29.path.includes('agents/bmad-tea/SKILL.md'),
|
||||
'Native type:agent SKILL.md path points to the agent directory entrypoint',
|
||||
);
|
||||
const nativeAgentManifest29 = generator29.agents.find((a) => a.name === 'bmad-tea');
|
||||
assert(nativeAgentManifest29 !== undefined, 'Native type:agent SKILL.md dir appears in agents[] for agent metadata');
|
||||
|
||||
// Regular type:workflow should NOT appear in skills[]
|
||||
const regularInSkills29 = generator29.skills.find((s) => s.canonicalId === 'regular-wf');
|
||||
|
|
|
|||
|
|
@ -54,8 +54,11 @@ class InstallPaths {
|
|||
manifestFile() {
|
||||
return path.join(this.configDir, 'manifest.yaml');
|
||||
}
|
||||
agentManifest() {
|
||||
return path.join(this.configDir, 'agent-manifest.csv');
|
||||
centralConfig() {
|
||||
return path.join(this.bmadDir, 'config.toml');
|
||||
}
|
||||
centralUserConfig() {
|
||||
return path.join(this.bmadDir, 'config.user.toml');
|
||||
}
|
||||
filesManifest() {
|
||||
return path.join(this.configDir, 'files-manifest.csv');
|
||||
|
|
|
|||
|
|
@ -310,7 +310,8 @@ class Installer {
|
|||
addResult('Configurations', 'ok', 'generated');
|
||||
|
||||
this.installedFiles.add(paths.manifestFile());
|
||||
this.installedFiles.add(paths.agentManifest());
|
||||
this.installedFiles.add(paths.centralConfig());
|
||||
this.installedFiles.add(paths.centralUserConfig());
|
||||
|
||||
message('Generating manifests...');
|
||||
const manifestGen = new ManifestGenerator();
|
||||
|
|
@ -331,10 +332,11 @@ class Installer {
|
|||
await manifestGen.generateManifests(paths.bmadDir, allModulesForManifest, [...this.installedFiles], {
|
||||
ides: config.ides || [],
|
||||
preservedModules: modulesForCsvPreserve,
|
||||
moduleConfigs,
|
||||
});
|
||||
|
||||
message('Generating help catalog...');
|
||||
await this.mergeModuleHelpCatalogs(paths.bmadDir);
|
||||
await this.mergeModuleHelpCatalogs(paths.bmadDir, manifestGen.agents);
|
||||
addResult('Help catalog', 'ok');
|
||||
|
||||
return 'Configurations generated';
|
||||
|
|
@ -922,47 +924,31 @@ class Installer {
|
|||
}
|
||||
|
||||
/**
|
||||
* Merge all module-help.csv files into a single bmad-help.csv
|
||||
* Scans all installed modules for module-help.csv and merges them
|
||||
* Enriches agent info from agent-manifest.csv
|
||||
* Output is written to _bmad/_config/bmad-help.csv
|
||||
* Merge all module-help.csv files into a single bmad-help.csv.
|
||||
* Scans all installed modules for module-help.csv and merges them.
|
||||
* Enriches agent info from the in-memory agent list produced by ManifestGenerator.
|
||||
* Output is written to _bmad/_config/bmad-help.csv.
|
||||
* @param {string} bmadDir - BMAD installation directory
|
||||
* @param {Array<Object>} agentEntries - Agents collected from module.yaml (code, name, title, icon, module, ...)
|
||||
*/
|
||||
async mergeModuleHelpCatalogs(bmadDir) {
|
||||
async mergeModuleHelpCatalogs(bmadDir, agentEntries = []) {
|
||||
const allRows = [];
|
||||
const headerRow =
|
||||
'module,phase,name,code,sequence,workflow-file,command,required,agent-name,agent-command,agent-display-name,agent-title,options,description,output-location,outputs';
|
||||
|
||||
// Load agent manifest for agent info lookup
|
||||
const agentManifestPath = path.join(bmadDir, '_config', 'agent-manifest.csv');
|
||||
const agentInfo = new Map(); // agent-name -> {command, displayName, title+icon}
|
||||
|
||||
if (await fs.pathExists(agentManifestPath)) {
|
||||
const manifestContent = await fs.readFile(agentManifestPath, 'utf8');
|
||||
const lines = manifestContent.split('\n').filter((line) => line.trim());
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.startsWith('name,')) continue; // Skip header
|
||||
|
||||
const cols = line.split(',');
|
||||
if (cols.length >= 4) {
|
||||
const agentName = cols[0].replaceAll('"', '').trim();
|
||||
const displayName = cols[1].replaceAll('"', '').trim();
|
||||
const title = cols[2].replaceAll('"', '').trim();
|
||||
const icon = cols[3].replaceAll('"', '').trim();
|
||||
const module = cols[10] ? cols[10].replaceAll('"', '').trim() : '';
|
||||
|
||||
// Build agent command: bmad:module:agent:name
|
||||
const agentCommand = module ? `bmad:${module}:agent:${agentName}` : `bmad:agent:${agentName}`;
|
||||
|
||||
agentInfo.set(agentName, {
|
||||
// Build agent lookup from the in-memory list (agent code → command + display fields).
|
||||
const agentInfo = new Map();
|
||||
for (const agent of agentEntries) {
|
||||
if (!agent || !agent.code) continue;
|
||||
const agentCommand = agent.module ? `bmad:${agent.module}:agent:${agent.code}` : `bmad:agent:${agent.code}`;
|
||||
const displayName = agent.name || agent.code;
|
||||
const titleCombined = agent.icon && agent.title ? `${agent.icon} ${agent.title}` : agent.title || agent.code;
|
||||
agentInfo.set(agent.code, {
|
||||
command: agentCommand,
|
||||
displayName: displayName || agentName,
|
||||
title: icon && title ? `${icon} ${title}` : title || agentName,
|
||||
displayName,
|
||||
title: titleCombined,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get all installed module directories
|
||||
const entries = await fs.readdir(bmadDir, { withFileTypes: true });
|
||||
|
|
|
|||
|
|
@ -2,14 +2,8 @@ const path = require('node:path');
|
|||
const fs = require('../fs-native');
|
||||
const yaml = require('yaml');
|
||||
const crypto = require('node:crypto');
|
||||
const csv = require('csv-parse/sync');
|
||||
const { getSourcePath, getModulePath } = require('../project-root');
|
||||
const { getModulePath } = require('../project-root');
|
||||
const prompts = require('../prompts');
|
||||
const {
|
||||
loadSkillManifest: loadSkillManifestShared,
|
||||
getCanonicalId: getCanonicalIdShared,
|
||||
getArtifactType: getArtifactTypeShared,
|
||||
} = require('../ide/shared/skill-manifest');
|
||||
|
||||
// Load package.json for version info
|
||||
const packageJson = require('../../../package.json');
|
||||
|
|
@ -26,21 +20,6 @@ class ManifestGenerator {
|
|||
this.selectedIdes = [];
|
||||
}
|
||||
|
||||
/** Delegate to shared skill-manifest module */
|
||||
async loadSkillManifest(dirPath) {
|
||||
return loadSkillManifestShared(dirPath);
|
||||
}
|
||||
|
||||
/** Delegate to shared skill-manifest module */
|
||||
getCanonicalId(manifest, filename) {
|
||||
return getCanonicalIdShared(manifest, filename);
|
||||
}
|
||||
|
||||
/** Delegate to shared skill-manifest module */
|
||||
getArtifactType(manifest, filename) {
|
||||
return getArtifactTypeShared(manifest, filename);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean text for CSV output by normalizing whitespace.
|
||||
* Note: Quote escaping is handled by escapeCsv() at write time.
|
||||
|
|
@ -98,17 +77,21 @@ class ManifestGenerator {
|
|||
// Collect skills first (populates skillClaimedDirs before legacy collectors run)
|
||||
await this.collectSkills();
|
||||
|
||||
// Collect agent data - use updatedModules which includes all installed modules
|
||||
await this.collectAgents(this.updatedModules);
|
||||
// Collect agent essence from each module's source module.yaml `agents:` array
|
||||
await this.collectAgentsFromModuleYaml();
|
||||
|
||||
// Write manifest files and collect their paths
|
||||
const [teamConfigPath, userConfigPath] = await this.writeCentralConfig(bmadDir, options.moduleConfigs || {});
|
||||
const manifestFiles = [
|
||||
await this.writeMainManifest(cfgDir),
|
||||
await this.writeSkillManifest(cfgDir),
|
||||
await this.writeAgentManifest(cfgDir),
|
||||
teamConfigPath,
|
||||
userConfigPath,
|
||||
await this.writeFilesManifest(cfgDir),
|
||||
];
|
||||
|
||||
await this.ensureCustomConfigStubs(bmadDir);
|
||||
|
||||
return {
|
||||
skills: this.skills.length,
|
||||
agents: this.agents.length,
|
||||
|
|
@ -150,24 +133,13 @@ class ManifestGenerator {
|
|||
const skillMeta = await this.parseSkillMd(skillMdPath, dir, dirName, debug);
|
||||
|
||||
if (skillMeta) {
|
||||
// Load manifest when present (for agent metadata)
|
||||
const manifest = await this.loadSkillManifest(dir);
|
||||
const artifactType = this.getArtifactType(manifest, skillFile);
|
||||
|
||||
// Build path relative from module root (points to SKILL.md — the permanent entrypoint)
|
||||
const relativePath = path.relative(modulePath, dir).split(path.sep).join('/');
|
||||
const installPath = relativePath
|
||||
? `${this.bmadFolderName}/${moduleName}/${relativePath}/${skillFile}`
|
||||
: `${this.bmadFolderName}/${moduleName}/${skillFile}`;
|
||||
|
||||
// Native SKILL.md entrypoints derive canonicalId from directory name.
|
||||
// Agent entrypoints may keep canonicalId metadata for compatibility, so
|
||||
// only warn for non-agent SKILL.md directories.
|
||||
if (manifest && manifest.__single && manifest.__single.canonicalId && artifactType !== 'agent') {
|
||||
console.warn(
|
||||
`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)`,
|
||||
);
|
||||
}
|
||||
// Native SKILL.md entrypoints always derive canonicalId from directory name.
|
||||
const canonicalId = dirName;
|
||||
|
||||
this.skills.push({
|
||||
|
|
@ -263,105 +235,49 @@ class ManifestGenerator {
|
|||
}
|
||||
|
||||
/**
|
||||
* Collect all agents from selected modules by walking their directory trees.
|
||||
* Collect agents from each installed module's source module.yaml `agents:` array.
|
||||
* Essence fields (code, name, title, icon, description) are authored in module.yaml;
|
||||
* `team` defaults to module code when not set; `module` is always the owning module.
|
||||
*/
|
||||
async collectAgents(selectedModules) {
|
||||
async collectAgentsFromModuleYaml() {
|
||||
this.agents = [];
|
||||
const debug = process.env.BMAD_DEBUG_MANIFEST === 'true';
|
||||
|
||||
// Walk each module's full directory tree looking for type:agent manifests
|
||||
for (const moduleName of this.updatedModules) {
|
||||
const modulePath = path.join(this.bmadDir, moduleName);
|
||||
if (!(await fs.pathExists(modulePath))) continue;
|
||||
const moduleYamlPath = path.join(getModulePath(moduleName), 'module.yaml');
|
||||
if (!(await fs.pathExists(moduleYamlPath))) continue;
|
||||
|
||||
const moduleAgents = await this.getAgentsFromDirRecursive(modulePath, moduleName, '', debug);
|
||||
this.agents.push(...moduleAgents);
|
||||
}
|
||||
|
||||
// Get standalone agents from bmad/agents/ directory
|
||||
const standaloneAgentsDir = path.join(this.bmadDir, 'agents');
|
||||
if (await fs.pathExists(standaloneAgentsDir)) {
|
||||
const standaloneAgents = await this.getAgentsFromDirRecursive(standaloneAgentsDir, 'standalone', '', debug);
|
||||
this.agents.push(...standaloneAgents);
|
||||
}
|
||||
|
||||
if (debug) {
|
||||
console.log(`[DEBUG] collectAgents: total agents found: ${this.agents.length}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively walk a directory tree collecting agents.
|
||||
* Discovers agents via directory with bmad-skill-manifest.yaml containing type: agent
|
||||
*
|
||||
* @param {string} dirPath - Current directory being scanned
|
||||
* @param {string} moduleName - Module this directory belongs to
|
||||
* @param {string} relativePath - Path relative to the module root (for install path construction)
|
||||
* @param {boolean} debug - Emit debug messages
|
||||
*/
|
||||
async getAgentsFromDirRecursive(dirPath, moduleName, relativePath = '', debug = false) {
|
||||
const agents = [];
|
||||
let entries;
|
||||
let moduleDef;
|
||||
try {
|
||||
entries = await fs.readdir(dirPath, { withFileTypes: true });
|
||||
} catch {
|
||||
return agents;
|
||||
}
|
||||
|
||||
for (const entry of entries) {
|
||||
if (!entry.isDirectory()) continue;
|
||||
if (entry.name.startsWith('.') || entry.name.startsWith('_')) continue;
|
||||
|
||||
const fullPath = path.join(dirPath, entry.name);
|
||||
|
||||
// Check for type:agent manifest BEFORE checking skillClaimedDirs —
|
||||
// agent dirs may be claimed by collectSkills for IDE installation,
|
||||
// but we still need them in agent-manifest.csv.
|
||||
const dirManifest = await this.loadSkillManifest(fullPath);
|
||||
if (dirManifest && dirManifest.__single && dirManifest.__single.type === 'agent') {
|
||||
const m = dirManifest.__single;
|
||||
const dirRelativePath = relativePath ? `${relativePath}/${entry.name}` : entry.name;
|
||||
const agentModule = m.module || moduleName;
|
||||
const installPath = `${this.bmadFolderName}/${agentModule}/${dirRelativePath}`;
|
||||
|
||||
agents.push({
|
||||
name: m.name || entry.name,
|
||||
displayName: m.displayName || m.name || entry.name,
|
||||
title: m.title || '',
|
||||
icon: m.icon || '',
|
||||
role: m.role ? this.cleanForCSV(m.role) : '',
|
||||
identity: m.identity ? this.cleanForCSV(m.identity) : '',
|
||||
communicationStyle: m.communicationStyle ? this.cleanForCSV(m.communicationStyle) : '',
|
||||
principles: m.principles ? this.cleanForCSV(m.principles) : '',
|
||||
module: agentModule,
|
||||
path: installPath,
|
||||
canonicalId: m.canonicalId || '',
|
||||
});
|
||||
|
||||
this.files.push({
|
||||
type: 'agent',
|
||||
name: m.name || entry.name,
|
||||
module: agentModule,
|
||||
path: installPath,
|
||||
});
|
||||
|
||||
if (debug) {
|
||||
console.log(`[DEBUG] collectAgents: found type:agent "${m.name || entry.name}" at ${fullPath}`);
|
||||
}
|
||||
moduleDef = yaml.parse(await fs.readFile(moduleYamlPath, 'utf8'));
|
||||
} catch (error) {
|
||||
if (debug) console.log(`[DEBUG] collectAgentsFromModuleYaml: failed to parse ${moduleYamlPath}: ${error.message}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip directories claimed by collectSkills (non-agent type skills) —
|
||||
// avoids recursing into skill trees that can't contain agents.
|
||||
if (this.skillClaimedDirs && this.skillClaimedDirs.has(fullPath)) continue;
|
||||
if (!moduleDef || !Array.isArray(moduleDef.agents)) continue;
|
||||
|
||||
// Recurse into subdirectories
|
||||
const newRelativePath = relativePath ? `${relativePath}/${entry.name}` : entry.name;
|
||||
const subDirAgents = await this.getAgentsFromDirRecursive(fullPath, moduleName, newRelativePath, debug);
|
||||
agents.push(...subDirAgents);
|
||||
for (const entry of moduleDef.agents) {
|
||||
if (!entry || typeof entry.code !== 'string') continue;
|
||||
this.agents.push({
|
||||
code: entry.code,
|
||||
name: entry.name || '',
|
||||
title: entry.title || '',
|
||||
icon: entry.icon || '',
|
||||
description: entry.description || '',
|
||||
module: moduleName,
|
||||
team: entry.team || moduleName,
|
||||
});
|
||||
}
|
||||
|
||||
return agents;
|
||||
if (debug) {
|
||||
console.log(`[DEBUG] collectAgentsFromModuleYaml: ${moduleName} contributed ${moduleDef.agents.length} agents`);
|
||||
}
|
||||
}
|
||||
|
||||
if (debug) {
|
||||
console.log(`[DEBUG] collectAgentsFromModuleYaml: total agents found: ${this.agents.length}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -477,75 +393,170 @@ class ManifestGenerator {
|
|||
}
|
||||
|
||||
/**
|
||||
* Write agent manifest CSV
|
||||
* @returns {string} Path to the manifest file
|
||||
* Write central _bmad/config.toml with [core], [modules.<code>], [agents.<code>] tables.
|
||||
* Install-owned. Team-scope answers → config.toml; user-scope answers → config.user.toml.
|
||||
* Both files are regenerated on every install. User overrides live in
|
||||
* _bmad/custom/config.toml and _bmad/custom/config.user.toml (never touched by installer).
|
||||
* @returns {string[]} Paths to the written config files
|
||||
*/
|
||||
async writeAgentManifest(cfgDir) {
|
||||
const csvPath = path.join(cfgDir, 'agent-manifest.csv');
|
||||
const escapeCsv = (value) => `"${String(value ?? '').replaceAll('"', '""')}"`;
|
||||
async writeCentralConfig(bmadDir, moduleConfigs) {
|
||||
const teamPath = path.join(bmadDir, 'config.toml');
|
||||
const userPath = path.join(bmadDir, 'config.user.toml');
|
||||
|
||||
// Read existing manifest to preserve entries
|
||||
const existingEntries = new Map();
|
||||
if (await fs.pathExists(csvPath)) {
|
||||
const content = await fs.readFile(csvPath, 'utf8');
|
||||
const records = csv.parse(content, {
|
||||
columns: true,
|
||||
skip_empty_lines: true,
|
||||
});
|
||||
for (const record of records) {
|
||||
existingEntries.set(`${record.module}:${record.name}`, record);
|
||||
// Load each module's source module.yaml to determine scope per prompt key.
|
||||
// Default scope is 'team' when the prompt doesn't declare one.
|
||||
const scopeByModuleKey = {};
|
||||
for (const moduleName of this.updatedModules) {
|
||||
const moduleYamlPath = path.join(getModulePath(moduleName), 'module.yaml');
|
||||
if (!(await fs.pathExists(moduleYamlPath))) continue;
|
||||
try {
|
||||
const parsed = yaml.parse(await fs.readFile(moduleYamlPath, 'utf8'));
|
||||
if (!parsed || typeof parsed !== 'object') continue;
|
||||
scopeByModuleKey[moduleName] = {};
|
||||
for (const [key, value] of Object.entries(parsed)) {
|
||||
if (value && typeof value === 'object' && 'prompt' in value) {
|
||||
scopeByModuleKey[moduleName][key] = value.scope === 'user' ? 'user' : 'team';
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Silently skip unparseable module.yaml — default-team behavior applies
|
||||
}
|
||||
}
|
||||
|
||||
// Create CSV header with persona fields and canonicalId
|
||||
let csvContent = 'name,displayName,title,icon,role,identity,communicationStyle,principles,module,path,canonicalId\n';
|
||||
const partition = (moduleName, cfg) => {
|
||||
const team = {};
|
||||
const user = {};
|
||||
const scopes = scopeByModuleKey[moduleName] || {};
|
||||
for (const [key, value] of Object.entries(cfg || {})) {
|
||||
if (scopes[key] === 'user') {
|
||||
user[key] = value;
|
||||
} else {
|
||||
team[key] = value;
|
||||
}
|
||||
}
|
||||
return { team, user };
|
||||
};
|
||||
|
||||
// Combine existing and new agents, preferring new data for duplicates
|
||||
const allAgents = new Map();
|
||||
const teamHeader = [
|
||||
'# ─────────────────────────────────────────────────────────────────',
|
||||
'# DO NOT EDIT — regenerated on every install.',
|
||||
'#',
|
||||
'# To override any value, add it to one of:',
|
||||
'# _bmad/custom/config.toml (team, committed to version control)',
|
||||
'# _bmad/custom/config.user.toml (personal, gitignored)',
|
||||
'# ─────────────────────────────────────────────────────────────────',
|
||||
'',
|
||||
];
|
||||
|
||||
// Add existing entries
|
||||
for (const [key, value] of existingEntries) {
|
||||
allAgents.set(key, value);
|
||||
const userHeader = [
|
||||
'# ─────────────────────────────────────────────────────────────────',
|
||||
'# DO NOT EDIT — regenerated on every install.',
|
||||
'# This file holds install answers scoped to YOU personally.',
|
||||
'#',
|
||||
'# To override any value, add it to:',
|
||||
'# _bmad/custom/config.user.toml (personal, gitignored)',
|
||||
'# ─────────────────────────────────────────────────────────────────',
|
||||
'',
|
||||
];
|
||||
|
||||
const teamLines = [...teamHeader];
|
||||
const userLines = [...userHeader];
|
||||
|
||||
// [core] — split into team and user
|
||||
const coreConfig = moduleConfigs.core || {};
|
||||
const { team: coreTeam, user: coreUser } = partition('core', coreConfig);
|
||||
if (Object.keys(coreTeam).length > 0) {
|
||||
teamLines.push('[core]');
|
||||
for (const [key, value] of Object.entries(coreTeam)) {
|
||||
teamLines.push(`${key} = ${formatTomlValue(value)}`);
|
||||
}
|
||||
teamLines.push('');
|
||||
}
|
||||
if (Object.keys(coreUser).length > 0) {
|
||||
userLines.push('[core]');
|
||||
for (const [key, value] of Object.entries(coreUser)) {
|
||||
userLines.push(`${key} = ${formatTomlValue(value)}`);
|
||||
}
|
||||
userLines.push('');
|
||||
}
|
||||
|
||||
// Add/update new agents
|
||||
// [modules.<code>] — split per module
|
||||
for (const moduleName of this.updatedModules) {
|
||||
if (moduleName === 'core') continue;
|
||||
const cfg = moduleConfigs[moduleName];
|
||||
if (!cfg || Object.keys(cfg).length === 0) continue;
|
||||
const { team: modTeam, user: modUser } = partition(moduleName, cfg);
|
||||
if (Object.keys(modTeam).length > 0) {
|
||||
teamLines.push(`[modules.${moduleName}]`);
|
||||
for (const [key, value] of Object.entries(modTeam)) {
|
||||
teamLines.push(`${key} = ${formatTomlValue(value)}`);
|
||||
}
|
||||
teamLines.push('');
|
||||
}
|
||||
if (Object.keys(modUser).length > 0) {
|
||||
userLines.push(`[modules.${moduleName}]`);
|
||||
for (const [key, value] of Object.entries(modUser)) {
|
||||
userLines.push(`${key} = ${formatTomlValue(value)}`);
|
||||
}
|
||||
userLines.push('');
|
||||
}
|
||||
}
|
||||
|
||||
// [agents.<code>] — always team (agent roster is organizational)
|
||||
for (const agent of this.agents) {
|
||||
const key = `${agent.module}:${agent.name}`;
|
||||
allAgents.set(key, {
|
||||
name: agent.name,
|
||||
displayName: agent.displayName,
|
||||
title: agent.title,
|
||||
icon: agent.icon,
|
||||
role: agent.role,
|
||||
identity: agent.identity,
|
||||
communicationStyle: agent.communicationStyle,
|
||||
principles: agent.principles,
|
||||
module: agent.module,
|
||||
path: agent.path,
|
||||
canonicalId: agent.canonicalId || '',
|
||||
});
|
||||
const agentLines = [`[agents.${agent.code}]`, `module = ${formatTomlValue(agent.module)}`, `team = ${formatTomlValue(agent.team)}`];
|
||||
if (agent.name) agentLines.push(`name = ${formatTomlValue(agent.name)}`);
|
||||
if (agent.title) agentLines.push(`title = ${formatTomlValue(agent.title)}`);
|
||||
if (agent.icon) agentLines.push(`icon = ${formatTomlValue(agent.icon)}`);
|
||||
if (agent.description) agentLines.push(`description = ${formatTomlValue(agent.description)}`);
|
||||
agentLines.push('');
|
||||
teamLines.push(...agentLines);
|
||||
}
|
||||
|
||||
// Write all agents
|
||||
for (const [, record] of allAgents) {
|
||||
const row = [
|
||||
escapeCsv(record.name),
|
||||
escapeCsv(record.displayName),
|
||||
escapeCsv(record.title),
|
||||
escapeCsv(record.icon),
|
||||
escapeCsv(record.role),
|
||||
escapeCsv(record.identity),
|
||||
escapeCsv(record.communicationStyle),
|
||||
escapeCsv(record.principles),
|
||||
escapeCsv(record.module),
|
||||
escapeCsv(record.path),
|
||||
escapeCsv(record.canonicalId),
|
||||
].join(',');
|
||||
csvContent += row + '\n';
|
||||
const teamContent = teamLines.join('\n').replace(/\n+$/, '\n');
|
||||
const userContent = userLines.join('\n').replace(/\n+$/, '\n');
|
||||
await fs.writeFile(teamPath, teamContent);
|
||||
await fs.writeFile(userPath, userContent);
|
||||
return [teamPath, userPath];
|
||||
}
|
||||
|
||||
await fs.writeFile(csvPath, csvContent);
|
||||
return csvPath;
|
||||
/**
|
||||
* Create empty _bmad/custom/config.toml and _bmad/custom/config.user.toml stubs
|
||||
* on first install only. Installer never touches these files again after creation.
|
||||
*/
|
||||
async ensureCustomConfigStubs(bmadDir) {
|
||||
const customDir = path.join(bmadDir, 'custom');
|
||||
await fs.ensureDir(customDir);
|
||||
|
||||
const stubs = [
|
||||
{
|
||||
file: path.join(customDir, 'config.toml'),
|
||||
header: [
|
||||
'# Team / enterprise overrides for _bmad/config.toml.',
|
||||
'# Committed to the repo — applies to every developer on the project.',
|
||||
'# Tables deep-merge over base config; keyed entries merge by key.',
|
||||
'# Example: override an agent descriptor, or add a new agent.',
|
||||
'#',
|
||||
'# [agents.bmad-agent-pm]',
|
||||
'# description = "Prefers short, bulleted PRDs over narrative drafts."',
|
||||
'',
|
||||
],
|
||||
},
|
||||
{
|
||||
file: path.join(customDir, 'config.user.toml'),
|
||||
header: [
|
||||
'# Personal overrides for _bmad/config.toml.',
|
||||
'# NOT committed (gitignored) — applies only to your local install.',
|
||||
'# Wins over both base config and team overrides.',
|
||||
'',
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
for (const { file, header } of stubs) {
|
||||
if (await fs.pathExists(file)) continue;
|
||||
await fs.writeFile(file, header.join('\n'));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -691,4 +702,24 @@ class ManifestGenerator {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a JS scalar as a TOML value literal.
|
||||
* Handles strings (quoted + escaped), booleans, numbers, and arrays of scalars.
|
||||
* Objects are not expected at this emit path.
|
||||
*/
|
||||
function formatTomlValue(value) {
|
||||
if (value === null || value === undefined) return '""';
|
||||
if (typeof value === 'boolean') return value ? 'true' : 'false';
|
||||
if (typeof value === 'number' && Number.isFinite(value)) return String(value);
|
||||
if (Array.isArray(value)) return `[${value.map((v) => formatTomlValue(v)).join(', ')}]`;
|
||||
const str = String(value);
|
||||
const escaped = str
|
||||
.replaceAll('\\', '\\\\')
|
||||
.replaceAll('"', String.raw`\"`)
|
||||
.replaceAll('\n', String.raw`\n`)
|
||||
.replaceAll('\r', String.raw`\r`)
|
||||
.replaceAll('\t', String.raw`\t`);
|
||||
return `"${escaped}"`;
|
||||
}
|
||||
|
||||
module.exports = { ManifestGenerator };
|
||||
|
|
|
|||
Loading…
Reference in New Issue