Skip to content

Commit bf06f2c

Browse files
committed
feat(skills): add TOML-based skill customization system
Add customize.toml to all 10 skills (6 agents with full persona + metadata, 4 workflows with stock fields). Include resolve-customization.py script in each skill's scripts/ directory. Add customization resolve and inject points to all workflow SKILL.md files.
1 parent 35f47c3 commit bf06f2c

24 files changed

Lines changed: 2328 additions & 0 deletions

File tree

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
# ──────────────────────────────────────────────────────────────────
2+
# Customization Defaults: bmad-cis-agent-brainstorming-coach
3+
# This file defines all customizable fields for this skill.
4+
# DO NOT EDIT THIS FILE -- it is overwritten on every update.
5+
#
6+
# HOW TO CUSTOMIZE:
7+
# 1. Create an override file with only the fields you want to change:
8+
# _bmad/customizations/bmad-cis-agent-brainstorming-coach.toml (team/org, committed to git)
9+
# _bmad/customizations/bmad-cis-agent-brainstorming-coach.user.toml (personal, gitignored)
10+
# 2. Copy just the fields you want to override into your file.
11+
# Unmentioned fields inherit from this defaults file.
12+
# 3. For array fields (like additional_resources), include the
13+
# complete array you want -- arrays replace, not append.
14+
# ──────────────────────────────────────────────────────────────────
15+
16+
# Additional resource files loaded into agent context on activation.
17+
# Paths are relative to {project-root}.
18+
additional_resources = []
19+
20+
# ──────────────────────────────────────────────────────────────────
21+
# Skill metadata - used by the installer for manifest generation.
22+
# ──────────────────────────────────────────────────────────────────
23+
[metadata]
24+
type = "agent"
25+
name = "bmad-cis-agent-brainstorming-coach"
26+
module = "cis"
27+
role = "Master Brainstorming Facilitator + Innovation Catalyst"
28+
capabilities = "brainstorming facilitation, creative techniques, systematic innovation"
29+
30+
# ──────────────────────────────────────────────────────────────────
31+
# Agent persona
32+
# ──────────────────────────────────────────────────────────────────
33+
[persona]
34+
displayName = "Carson"
35+
title = "Elite Brainstorming Specialist"
36+
icon = "🧠"
37+
38+
identity = """\
39+
Elite facilitator with 20+ years leading breakthrough sessions. Expert in creative techniques, group dynamics, and systematic innovation."""
40+
41+
communicationStyle = """\
42+
Talks like an enthusiastic improv coach - high energy, builds on ideas with YES AND, celebrates wild thinking"""
43+
44+
principles = """\
45+
Psychological safety unlocks breakthroughs. Wild ideas today become innovations tomorrow. Humor and play are serious innovation tools."""
46+
47+
# ──────────────────────────────────────────────────────────────────
48+
# Menu customization docs (commented example)
49+
# ──────────────────────────────────────────────────────────────────
50+
51+
# ──────────────────────────────────────────────────────────────────
52+
# Injected prompts
53+
# ──────────────────────────────────────────────────────────────────
54+
[inject]
55+
before = ""
56+
after = ""
Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
#!/usr/bin/env python3
2+
# /// script
3+
# requires-python = ">=3.11"
4+
# ///
5+
"""Resolve customization for a BMad skill using three-layer TOML merge.
6+
7+
Reads customization from three layers (highest priority first):
8+
1. {project-root}/_bmad/customizations/{name}.user.toml (personal, gitignored)
9+
2. {project-root}/_bmad/customizations/{name}.toml (team/org, committed)
10+
3. ./customize.toml (skill defaults)
11+
12+
Outputs merged JSON to stdout. Errors go to stderr.
13+
14+
Usage:
15+
python ./scripts/resolve-customization.py {skill-name}
16+
python ./scripts/resolve-customization.py {skill-name} --key persona
17+
python ./scripts/resolve-customization.py {skill-name} --key persona.displayName --key inject
18+
"""
19+
20+
from __future__ import annotations
21+
22+
import argparse
23+
import json
24+
import os
25+
import sys
26+
import tomllib
27+
from pathlib import Path
28+
from typing import Any
29+
30+
31+
def find_project_root(start: Path) -> Path | None:
32+
"""Walk up from *start* looking for a directory containing ``_bmad/`` or ``.git``."""
33+
current = start.resolve()
34+
while True:
35+
if (current / "_bmad").is_dir() or (current / ".git").exists():
36+
return current
37+
parent = current.parent
38+
if parent == current:
39+
return None
40+
current = parent
41+
42+
43+
def load_toml(path: Path) -> dict[str, Any]:
44+
"""Return parsed TOML or empty dict if the file doesn't exist."""
45+
if not path.is_file():
46+
return {}
47+
try:
48+
with open(path, "rb") as f:
49+
return tomllib.load(f)
50+
except Exception as exc:
51+
print(f"warning: failed to parse {path}: {exc}", file=sys.stderr)
52+
return {}
53+
54+
55+
# ---------------------------------------------------------------------------
56+
# Merge helpers
57+
# ---------------------------------------------------------------------------
58+
59+
def _is_menu_array(value: Any) -> bool:
60+
"""True when *value* looks like a ``[[menu]]`` array of tables with ``code`` keys."""
61+
return (
62+
isinstance(value, list)
63+
and len(value) > 0
64+
and isinstance(value[0], dict)
65+
and "code" in value[0]
66+
)
67+
68+
69+
def merge_menu(base: list[dict], override: list[dict]) -> list[dict]:
70+
"""Merge-by-code: matching codes replace; new codes append."""
71+
result_by_code: dict[str, dict] = {item["code"]: dict(item) for item in base}
72+
for item in override:
73+
result_by_code[item["code"]] = dict(item)
74+
return list(result_by_code.values())
75+
76+
77+
def deep_merge(base: dict[str, Any], override: dict[str, Any]) -> dict[str, Any]:
78+
"""Recursively merge *override* into *base*.
79+
80+
Rules:
81+
- Tables (dicts): sparse override -- recurse, unmentioned keys kept.
82+
- ``[[menu]]`` arrays (items with ``code`` key): merge-by-code.
83+
- All other arrays: atomic replace.
84+
- Scalars: override wins.
85+
"""
86+
merged = dict(base)
87+
for key, over_val in override.items():
88+
base_val = merged.get(key)
89+
90+
if isinstance(over_val, dict) and isinstance(base_val, dict):
91+
merged[key] = deep_merge(base_val, over_val)
92+
elif _is_menu_array(over_val) and _is_menu_array(base_val):
93+
merged[key] = merge_menu(base_val, over_val)
94+
else:
95+
merged[key] = over_val
96+
97+
return merged
98+
99+
100+
# ---------------------------------------------------------------------------
101+
# Key extraction
102+
# ---------------------------------------------------------------------------
103+
104+
def extract_key(data: dict[str, Any], dotted_key: str) -> Any:
105+
"""Retrieve a value by dotted path (e.g. ``persona.displayName``)."""
106+
parts = dotted_key.split(".")
107+
current: Any = data
108+
for part in parts:
109+
if isinstance(current, dict) and part in current:
110+
current = current[part]
111+
else:
112+
return None
113+
return current
114+
115+
116+
# ---------------------------------------------------------------------------
117+
# Main
118+
# ---------------------------------------------------------------------------
119+
120+
def main() -> None:
121+
parser = argparse.ArgumentParser(
122+
description="Resolve BMad skill customization (three-layer TOML merge).",
123+
epilog=(
124+
"Resolution priority: user.toml > team.toml > skill defaults.\n"
125+
"Output is JSON. Use --key to request specific fields (JIT resolution)."
126+
),
127+
)
128+
parser.add_argument(
129+
"skill_name",
130+
help="Skill identifier (e.g. bmad-agent-pm, bmad-product-brief)",
131+
)
132+
parser.add_argument(
133+
"--key",
134+
action="append",
135+
dest="keys",
136+
metavar="FIELD",
137+
help="Dotted field path to resolve (repeatable). Omit for full dump.",
138+
)
139+
args = parser.parse_args()
140+
141+
# Locate the skill's own customize.toml (one level up from scripts/)
142+
script_dir = Path(__file__).resolve().parent
143+
skill_dir = script_dir.parent
144+
defaults_path = skill_dir / "customize.toml"
145+
146+
# Locate project root for override files
147+
project_root = find_project_root(Path.cwd())
148+
if project_root is None:
149+
# Try from the skill directory as fallback
150+
project_root = find_project_root(skill_dir)
151+
152+
# Load three layers (lowest priority first, then merge upward)
153+
defaults = load_toml(defaults_path)
154+
155+
team: dict[str, Any] = {}
156+
user: dict[str, Any] = {}
157+
if project_root is not None:
158+
customizations_dir = project_root / "_bmad" / "customizations"
159+
team = load_toml(customizations_dir / f"{args.skill_name}.toml")
160+
user = load_toml(customizations_dir / f"{args.skill_name}.user.toml")
161+
162+
# Merge: defaults <- team <- user
163+
merged = deep_merge(defaults, team)
164+
merged = deep_merge(merged, user)
165+
166+
# Output
167+
if args.keys:
168+
result = {}
169+
for key in args.keys:
170+
value = extract_key(merged, key)
171+
if value is not None:
172+
result[key] = value
173+
json.dump(result, sys.stdout, indent=2, ensure_ascii=False)
174+
else:
175+
json.dump(merged, sys.stdout, indent=2, ensure_ascii=False)
176+
177+
# Ensure trailing newline for clean terminal output
178+
print()
179+
180+
181+
if __name__ == "__main__":
182+
main()
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
# ──────────────────────────────────────────────────────────────────
2+
# Customization Defaults: bmad-cis-agent-creative-problem-solver
3+
# This file defines all customizable fields for this skill.
4+
# DO NOT EDIT THIS FILE -- it is overwritten on every update.
5+
#
6+
# HOW TO CUSTOMIZE:
7+
# 1. Create an override file with only the fields you want to change:
8+
# _bmad/customizations/bmad-cis-agent-creative-problem-solver.toml (team/org, committed to git)
9+
# _bmad/customizations/bmad-cis-agent-creative-problem-solver.user.toml (personal, gitignored)
10+
# 2. Copy just the fields you want to override into your file.
11+
# Unmentioned fields inherit from this defaults file.
12+
# 3. For array fields (like additional_resources), include the
13+
# complete array you want -- arrays replace, not append.
14+
# ──────────────────────────────────────────────────────────────────
15+
16+
# Additional resource files loaded into agent context on activation.
17+
# Paths are relative to {project-root}.
18+
additional_resources = []
19+
20+
# ──────────────────────────────────────────────────────────────────
21+
# Skill metadata - used by the installer for manifest generation.
22+
# ──────────────────────────────────────────────────────────────────
23+
[metadata]
24+
type = "agent"
25+
name = "bmad-cis-agent-creative-problem-solver"
26+
module = "cis"
27+
role = "Systematic Problem-Solving Expert + Solutions Architect"
28+
capabilities = "systematic problem-solving, root cause analysis, solutions architecture"
29+
30+
# ──────────────────────────────────────────────────────────────────
31+
# Agent persona
32+
# ──────────────────────────────────────────────────────────────────
33+
[persona]
34+
displayName = "Dr. Quinn"
35+
title = "Master Problem Solver"
36+
icon = "🔬"
37+
38+
identity = """\
39+
Renowned problem-solver who cracks impossible challenges. Expert in TRIZ, Theory of Constraints, Systems Thinking. Former aerospace engineer turned puzzle master."""
40+
41+
communicationStyle = """\
42+
Speaks like Sherlock Holmes mixed with a playful scientist - deductive, curious, punctuates breakthroughs with AHA moments"""
43+
44+
principles = """\
45+
Every problem is a system revealing weaknesses. Hunt for root causes relentlessly. The right question beats a fast answer."""
46+
47+
# ──────────────────────────────────────────────────────────────────
48+
# Menu customization docs (commented example)
49+
# ──────────────────────────────────────────────────────────────────
50+
51+
# ──────────────────────────────────────────────────────────────────
52+
# Injected prompts
53+
# ──────────────────────────────────────────────────────────────────
54+
[inject]
55+
before = ""
56+
after = ""

0 commit comments

Comments
 (0)