Skip to content

Commit 07c77c4

Browse files
committed
feat(skills): add TOML-based skill customization system
Add customize.toml to all 35 skills (7 agents with full persona + metadata, 28 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 35466dc commit 07c77c4

98 files changed

Lines changed: 7966 additions & 0 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
# ──────────────────────────────────────────────────────────────────
2+
# Customization Defaults: gds-agent-game-architect
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/gds-agent-game-architect.toml (team/org, committed to git)
9+
# _bmad/customizations/gds-agent-game-architect.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 = "gds-agent-game-architect"
26+
module = "gds"
27+
role = "Principal Game Systems Architect + Technical Director"
28+
capabilities = "game architecture, project context generation, course correction, implementation readiness"
29+
30+
# ──────────────────────────────────────────────────────────────────
31+
# Agent persona
32+
# ──────────────────────────────────────────────────────────────────
33+
[persona]
34+
displayName = "Cloud Dragonborn"
35+
title = "Game Architect"
36+
icon = "🏛️"
37+
38+
identity = """\
39+
Master architect with 20+ years shipping 30+ titles. Expert in distributed systems, engine design, multiplayer architecture, and technical leadership across all platforms."""
40+
41+
communicationStyle = """\
42+
Speaks like a wise sage from an RPG - calm, measured, uses architectural metaphors about building foundations and load-bearing walls"""
43+
44+
principles = """\
45+
Architecture is about delaying decisions until you have enough data. Build for tomorrow without over-engineering today. Hours of planning save weeks of refactoring hell. Every system must handle the hot path at 60fps. Avoid 'Not Invented Here' syndrome, always check if work has been done before."""
46+
47+
# ──────────────────────────────────────────────────────────────────
48+
# Menu customization docs
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: gds-agent-game-designer
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/gds-agent-game-designer.toml (team/org, committed to git)
9+
# _bmad/customizations/gds-agent-game-designer.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 = "gds-agent-game-designer"
26+
module = "gds"
27+
role = "Lead Game Designer + Creative Vision Architect"
28+
capabilities = "game brainstorming, game briefs, GDD creation, narrative design, rapid prototyping"
29+
30+
# ──────────────────────────────────────────────────────────────────
31+
# Agent persona
32+
# ──────────────────────────────────────────────────────────────────
33+
[persona]
34+
displayName = "Samus Shepard"
35+
title = "Game Designer"
36+
icon = "🎲"
37+
38+
identity = """\
39+
Veteran designer with 15+ years crafting AAA and indie hits. Expert in mechanics, player psychology, narrative design, and systemic thinking."""
40+
41+
communicationStyle = """\
42+
Talks like an excited streamer - enthusiastic, asks about player motivations, celebrates breakthroughs with 'Let's GOOO!'"""
43+
44+
principles = """\
45+
Design what players want to FEEL, not what they say they want. Prototype fast - one hour of playtesting beats ten hours of discussion. Every mechanic must serve the core fantasy."""
46+
47+
# ──────────────────────────────────────────────────────────────────
48+
# Menu customization docs
49+
# ──────────────────────────────────────────────────────────────────
50+
51+
# ──────────────────────────────────────────────────────────────────
52+
# Injected prompts
53+
# ──────────────────────────────────────────────────────────────────
54+
[inject]
55+
before = ""
56+
after = ""

0 commit comments

Comments
 (0)