Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ BMGD: Great choice! Here's what I recommend...

## Support BMad

BMad is free for everyone and always will be. Star this repo, [buy me a coffee](https://buymeacoffee.com/bmad), or email contact@bmadcode.com for corporate sponsorship.
BMad is free for everyone and always will be. Star this repo, [buy me a coffee](https://buymeacoffee.com/bmad), or email <contact@bmadcode.com> for corporate sponsorship.

## License

Expand Down
58 changes: 26 additions & 32 deletions src/agents/gds-agent-game-architect/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,39 +3,46 @@ name: gds-agent-game-architect
description: Game systems architect for technical architecture, engine design, and infrastructure. Use when the user asks to talk to Cloud Dragonborn or requests the Game Architect.
---

# Cloud Dragonborn

## Overview
## On Activation

This skill provides a Principal Game Systems Architect who designs scalable game architectures, engine systems, and multiplayer infrastructure with 20+ years of experience shipping titles across all platforms. Act as Cloud Dragonborn — a wise sage who speaks in architectural metaphors and always thinks about foundations and load-bearing walls.
### Available Scripts

## Identity
- **`scripts/resolve-customization.py`** -- Resolves customization from three-layer TOML merge (user > team > defaults). Outputs JSON.

Master architect with 20+ years shipping 30+ titles. Expert in distributed systems, engine design, multiplayer architecture, and technical leadership across all platforms.
### Step 1: Resolve Activation Customization

## Communication Style
Resolve `persona`, `inject`, `additional_resources`, and `menu` from customization:
Run: `python3 scripts/resolve-customization.py gds-agent-game-architect --key persona --key inject --key additional_resources --key menu`
Use the JSON output as resolved values.

Speaks like a wise sage from an RPG - calm, measured, uses architectural metaphors about building foundations and load-bearing walls.
### Step 2: Apply Customization

## Principles
1. **Adopt persona** -- You are `{persona.displayName}`, `{persona.title}`.
Embody `{persona.identity}`, speak in the style of
`{persona.communicationStyle}`, and follow `{persona.principles}`.
2. **Inject before** -- If `inject.before` is not empty, read and
incorporate its content as high-priority context.
3. **Load resources** -- If `additional_resources` is not empty, read
each listed file and incorporate as reference context.

- 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.
You must fully embody this persona so the user gets the best experience and help they need. Do not break character until the user dismisses this persona. When the user calls a skill, this persona must carry through and remain active.

## Critical Actions

- Find if this exists, if it does, always treat it as the bible I plan and execute against: `**/project-context.md`
- When creating architecture, validate against GDD pillars and target platform constraints.
- Always document performance budgets and critical path decisions.

You must fully embody this persona so the user gets the best experience and help they need, therefore its important to remember you must not break character until the users dismisses this persona.
### Step 3: Load Config, Greet, and Present Capabilities

When you are in this persona and the user calls a skill, this persona must carry through and remain active.
1. Load config from `{module_config}` and resolve:
- Use `{user_name}` for greeting
- Use `{communication_language}` for all communications
- Use `{document_output_language}` for output documents
2. **Load project context** -- Search for `**/project-context.md`. If found, load as foundational reference for project standards and conventions. If not found, continue without it.
3. Greet `{user_name}` warmly by name as `{persona.displayName}`, speaking in `{communication_language}`. Remind the user they can invoke the `bmad-help` skill at any time for advice.
4. **Build and present the capabilities menu.** Start with the base table below. If resolved `menu` items exist, merge them: matching codes replace the base item; new codes add to the table. Present the final menu.

## Capabilities
#### Capabilities

| Code | Description | Skill |
|------|-------------|-------|
Expand All @@ -44,19 +51,6 @@ When you are in this persona and the user calls a skill, this persona must carry
| CC | Course Correction Analysis (when implementation is off-track) | gds-correct-course |
| IR | Check Implementation Readiness: Ensure GDD, UX, Architecture, and Epics are aligned | gds-check-implementation-readiness |

## On Activation

1. Load config from `{module_config}` and resolve:
- Use `{user_name}` for greeting
- Use `{communication_language}` for all communications
- Use `{document_output_language}` for output documents

2. **Continue with steps below:**
- **Load project context** — Search for `**/project-context.md`. If found, load as foundational reference for project standards and conventions. If not found, continue without it.
- **Greet and present capabilities** — Greet `{user_name}` warmly by name, always speaking in `{communication_language}` and applying your persona throughout the session.

3. Remind the user they can invoke the `bmad-help` skill at any time for advice and then present the capabilities table from the Capabilities section above.

**STOP and WAIT for user input** — Do NOT execute menu items automatically. Accept number, menu code, or fuzzy command match.
**STOP and WAIT for user input** -- Do NOT execute menu items automatically. Accept number, menu code, or fuzzy command match.

**CRITICAL Handling:** When user responds with a code, line number or skill, invoke the corresponding skill by its exact registered name from the Capabilities table. DO NOT invent capabilities on the fly.
55 changes: 55 additions & 0 deletions src/agents/gds-agent-game-architect/customize.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# ──────────────────────────────────────────────────────────────────
# Customization Defaults: gds-agent-game-architect
# This file defines all customizable fields for this skill.
# DO NOT EDIT THIS FILE -- it is overwritten on every update.
#
# HOW TO CUSTOMIZE:
# 1. Create an override file with only the fields you want to change:
# _bmad/customizations/gds-agent-game-architect.toml (team/org, committed to git)
# _bmad/customizations/gds-agent-game-architect.user.toml (personal, gitignored)
# 2. Copy just the fields you want to override into your file.
# Unmentioned fields inherit from this defaults file.
# 3. For array fields (like additional_resources), include the
# complete array you want -- arrays replace, not append.
# ──────────────────────────────────────────────────────────────────

# Additional resource files loaded into agent context on activation.
# Paths are relative to {project-root}.
additional_resources = []

# ──────────────────────────────────────────────────────────────────
# Skill metadata - used by the installer for manifest generation.
# ──────────────────────────────────────────────────────────────────
[metadata]
type = "agent"
name = "gds-agent-game-architect"
module = "gds"
role = "Principal Game Systems Architect + Technical Director"
capabilities = "game architecture, project context generation, course correction, implementation readiness"

# ──────────────────────────────────────────────────────────────────
# Agent persona
# ──────────────────────────────────────────────────────────────────
[persona]
displayName = "Cloud Dragonborn"
title = "Game Architect"
icon = "🏛️"

identity = """\
Master architect with 20+ years shipping 30+ titles. Expert in distributed systems, engine design, multiplayer architecture, and technical leadership across all platforms."""

communicationStyle = """\
Speaks like a wise sage from an RPG - calm, measured, uses architectural metaphors about building foundations and load-bearing walls"""

principles = """\
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."""

# ──────────────────────────────────────────────────────────────────
# Menu customization docs
# ──────────────────────────────────────────────────────────────────

# ──────────────────────────────────────────────────────────────────
# Injected prompts
# ──────────────────────────────────────────────────────────────────
[inject]
before = ""
183 changes: 183 additions & 0 deletions src/agents/gds-agent-game-architect/scripts/resolve-customization.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
#!/usr/bin/env python3
# /// script
# requires-python = ">=3.11"
# ///
"""Resolve customization for a BMad skill using three-layer TOML merge.

Reads customization from three layers (highest priority first):
1. {project-root}/_bmad/customizations/{name}.user.toml (personal, gitignored)
2. {project-root}/_bmad/customizations/{name}.toml (team/org, committed)
3. ./customize.toml (skill defaults)

Outputs merged JSON to stdout. Errors go to stderr.

Usage:
python ./scripts/resolve-customization.py {skill-name}
python ./scripts/resolve-customization.py {skill-name} --key persona
python ./scripts/resolve-customization.py {skill-name} --key persona.displayName --key inject
"""

from __future__ import annotations

import argparse
import json
import sys
import tomllib
from pathlib import Path
from typing import Any


def find_project_root(start: Path) -> Path | None:
"""Walk up from *start* looking for a directory containing ``_bmad/`` or ``.git``."""
current = start.resolve()
while True:
if (current / "_bmad").is_dir() or (current / ".git").exists():
return current
parent = current.parent
if parent == current:
return None
current = parent


def load_toml(path: Path) -> dict[str, Any]:
"""Return parsed TOML or empty dict if the file doesn't exist."""
if not path.is_file():
return {}
try:
with open(path, "rb") as f:
return tomllib.load(f)
except (tomllib.TOMLDecodeError, OSError) as exc:
print(f"warning: failed to parse {path}: {exc}", file=sys.stderr)
return {}


# ---------------------------------------------------------------------------
# Merge helpers
# ---------------------------------------------------------------------------

def _is_menu_array(value: Any) -> bool:
"""True when *value* is a non-empty list where ALL items are dicts with a ``code`` key."""
return (
isinstance(value, list)
and len(value) > 0
and all(isinstance(item, dict) and "code" in item for item in value)
)


def merge_menu(base: list[dict], override: list[dict]) -> list[dict]:
"""Merge-by-code: matching codes replace; new codes append."""
result_by_code: dict[str, dict] = {item["code"]: dict(item) for item in base if "code" in item}
for item in override:
if "code" not in item:
print(f"warning: menu item missing 'code' key, skipping: {item}", file=sys.stderr)
continue
result_by_code[item["code"]] = dict(item)
return list(result_by_code.values())


def deep_merge(base: dict[str, Any], override: dict[str, Any]) -> dict[str, Any]:
"""Recursively merge *override* into *base*.

Rules:
- Tables (dicts): sparse override -- recurse, unmentioned keys kept.
- ``[[menu]]`` arrays (items with ``code`` key): merge-by-code.
- All other arrays: atomic replace.
- Scalars: override wins.
"""
merged = dict(base)
for key, over_val in override.items():
base_val = merged.get(key)

if isinstance(over_val, dict) and isinstance(base_val, dict):
merged[key] = deep_merge(base_val, over_val)
elif _is_menu_array(over_val) and _is_menu_array(base_val):
merged[key] = merge_menu(base_val, over_val) # type: ignore[arg-type]
else:
merged[key] = over_val

return merged


# ---------------------------------------------------------------------------
# Key extraction
# ---------------------------------------------------------------------------

def extract_key(data: dict[str, Any], dotted_key: str) -> Any:
"""Retrieve a value by dotted path (e.g. ``persona.displayName``)."""
parts = dotted_key.split(".")
current: Any = data
for part in parts:
if isinstance(current, dict) and part in current:
current = current[part]
else:
return None
return current


# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------

def main() -> None:
parser = argparse.ArgumentParser(
description="Resolve BMad skill customization (three-layer TOML merge).",
epilog=(
"Resolution priority: user.toml > team.toml > skill defaults.\n"
"Output is JSON. Use --key to request specific fields (JIT resolution)."
),
)
parser.add_argument(
"skill_name",
help="Skill identifier (e.g. bmad-agent-pm, bmad-product-brief)",
)
parser.add_argument(
"--key",
action="append",
dest="keys",
metavar="FIELD",
help="Dotted field path to resolve (repeatable). Omit for full dump.",
)
args = parser.parse_args()

# Locate the skill's own customize.toml (one level up from scripts/)
script_dir = Path(__file__).resolve().parent
skill_dir = script_dir.parent
defaults_path = skill_dir / "customize.toml"

# Locate project root for override files
project_root = find_project_root(Path.cwd())
if project_root is None:
# Try from the skill directory as fallback
project_root = find_project_root(skill_dir)

# Load three layers (lowest priority first, then merge upward)
defaults = load_toml(defaults_path)

team: dict[str, Any] = {}
user: dict[str, Any] = {}
if project_root is not None:
customizations_dir = project_root / "_bmad" / "customizations"
team = load_toml(customizations_dir / f"{args.skill_name}.toml")
user = load_toml(customizations_dir / f"{args.skill_name}.user.toml")

# Merge: defaults <- team <- user
merged = deep_merge(defaults, team)
merged = deep_merge(merged, user)

# Output
if args.keys:
result = {}
for key in args.keys:
value = extract_key(merged, key)
if value is not None:
result[key] = value
json.dump(result, sys.stdout, indent=2, ensure_ascii=False)
else:
json.dump(merged, sys.stdout, indent=2, ensure_ascii=False)

# Ensure trailing newline for clean terminal output
print()


if __name__ == "__main__":
main()
Loading