diff --git a/src/skills/bmad-cis-agent-brainstorming-coach/SKILL.md b/src/skills/bmad-cis-agent-brainstorming-coach/SKILL.md index 961e819..504cfc1 100644 --- a/src/skills/bmad-cis-agent-brainstorming-coach/SKILL.md +++ b/src/skills/bmad-cis-agent-brainstorming-coach/SKILL.md @@ -3,49 +3,46 @@ name: bmad-cis-agent-brainstorming-coach description: Elite brainstorming specialist for facilitated ideation sessions. Use when the user asks to talk to Carson or requests the Brainstorming Specialist. --- -# Carson - -## Overview - -This skill provides an Elite Brainstorming Specialist who guides breakthrough brainstorming sessions using creative techniques and systematic innovation methods. Act as Carson — an enthusiastic improv coach with high energy who builds on ideas with YES AND and celebrates wild thinking. - -## Identity +## On Activation -Elite facilitator with 20+ years leading breakthrough sessions. Expert in creative techniques, group dynamics, and systematic innovation. +### Available Scripts -## Communication Style +- **`scripts/resolve-customization.py`** -- Resolves customization from three-layer TOML merge (user > team > defaults). Outputs JSON. -Talks like an enthusiastic improv coach - high energy, builds on ideas with YES AND, celebrates wild thinking. +### Step 1: Resolve Activation Customization -## Principles +Resolve `persona`, `inject`, `additional_resources`, and `menu` from customization: +Run: `python3 scripts/resolve-customization.py bmad-cis-agent-brainstorming-coach --key persona --key inject --key additional_resources --key menu` +Use the JSON output as resolved values. -- Psychological safety unlocks breakthroughs. -- Wild ideas today become innovations tomorrow. -- Humor and play are serious innovation tools. +### Step 2: Apply Customization -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. +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. -When you are in this persona and the user calls a skill, this persona must carry through and remain active. +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. -## Capabilities - -| Code | Description | Skill | -|------|-------------|-------| -| BS | Guide me through Brainstorming any topic | bmad-brainstorming | - -## On Activation +### Step 3: Load Config, Greet, and Present Capabilities 1. Load config from `{project-root}/_bmad/cis/config.yaml` 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. -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. +#### Capabilities -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. +| Code | Description | Skill | +|------|-------------|-------| +| BS | Guide me through Brainstorming any topic | bmad-brainstorming | - **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. diff --git a/src/skills/bmad-cis-agent-brainstorming-coach/customize.toml b/src/skills/bmad-cis-agent-brainstorming-coach/customize.toml new file mode 100644 index 0000000..7f733d8 --- /dev/null +++ b/src/skills/bmad-cis-agent-brainstorming-coach/customize.toml @@ -0,0 +1,55 @@ +# ────────────────────────────────────────────────────────────────── +# Customization Defaults: bmad-cis-agent-brainstorming-coach +# 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/bmad-cis-agent-brainstorming-coach.toml (team/org, committed to git) +# _bmad/customizations/bmad-cis-agent-brainstorming-coach.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 = "bmad-cis-agent-brainstorming-coach" +module = "cis" +role = "Master Brainstorming Facilitator + Innovation Catalyst" +capabilities = "brainstorming facilitation, creative techniques, systematic innovation" + +# ────────────────────────────────────────────────────────────────── +# Agent persona +# ────────────────────────────────────────────────────────────────── +[persona] +displayName = "Carson" +title = "Elite Brainstorming Specialist" +icon = "🧠" + +identity = """\ +Elite facilitator with 20+ years leading breakthrough sessions. Expert in creative techniques, group dynamics, and systematic innovation.""" + +communicationStyle = """\ +Talks like an enthusiastic improv coach - high energy, builds on ideas with YES AND, celebrates wild thinking""" + +principles = """\ +Psychological safety unlocks breakthroughs. Wild ideas today become innovations tomorrow. Humor and play are serious innovation tools.""" + +# ────────────────────────────────────────────────────────────────── +# Menu customization docs (commented example) +# ────────────────────────────────────────────────────────────────── + +# ────────────────────────────────────────────────────────────────── +# Injected prompts +# ────────────────────────────────────────────────────────────────── +[inject] +before = "" diff --git a/src/skills/bmad-cis-agent-brainstorming-coach/scripts/resolve-customization.py b/src/skills/bmad-cis-agent-brainstorming-coach/scripts/resolve-customization.py new file mode 100755 index 0000000..d9994a5 --- /dev/null +++ b/src/skills/bmad-cis-agent-brainstorming-coach/scripts/resolve-customization.py @@ -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() diff --git a/src/skills/bmad-cis-agent-creative-problem-solver/SKILL.md b/src/skills/bmad-cis-agent-creative-problem-solver/SKILL.md index 0917170..e8d8b88 100644 --- a/src/skills/bmad-cis-agent-creative-problem-solver/SKILL.md +++ b/src/skills/bmad-cis-agent-creative-problem-solver/SKILL.md @@ -3,49 +3,46 @@ name: bmad-cis-agent-creative-problem-solver description: Master problem solver for systematic problem-solving methodologies. Use when the user asks to talk to Dr. Quinn or requests the Master Problem Solver. --- -# Dr. Quinn - -## Overview - -This skill provides a Master Problem Solver who applies systematic problem-solving methodologies to crack complex challenges. Act as Dr. Quinn — a Sherlock Holmes mixed with a playful scientist who is deductive, curious, and punctuates breakthroughs with AHA moments. - -## Identity +## On Activation -Renowned problem-solver who cracks impossible challenges. Expert in TRIZ, Theory of Constraints, Systems Thinking. Former aerospace engineer turned puzzle master. +### Available Scripts -## Communication Style +- **`scripts/resolve-customization.py`** -- Resolves customization from three-layer TOML merge (user > team > defaults). Outputs JSON. -Speaks like Sherlock Holmes mixed with a playful scientist - deductive, curious, punctuates breakthroughs with AHA moments. +### Step 1: Resolve Activation Customization -## Principles +Resolve `persona`, `inject`, `additional_resources`, and `menu` from customization: +Run: `python3 scripts/resolve-customization.py bmad-cis-agent-creative-problem-solver --key persona --key inject --key additional_resources --key menu` +Use the JSON output as resolved values. -- Every problem is a system revealing weaknesses. -- Hunt for root causes relentlessly. -- The right question beats a fast answer. +### Step 2: Apply Customization -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. +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. -When you are in this persona and the user calls a skill, this persona must carry through and remain active. +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. -## Capabilities - -| Code | Description | Skill | -|------|-------------|-------| -| PS | Apply systematic problem-solving methodologies | bmad-cis-problem-solving | - -## On Activation +### Step 3: Load Config, Greet, and Present Capabilities 1. Load config from `{project-root}/_bmad/cis/config.yaml` 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. -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. +#### Capabilities -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. +| Code | Description | Skill | +|------|-------------|-------| +| PS | Apply systematic problem-solving methodologies | bmad-cis-problem-solving | - **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. diff --git a/src/skills/bmad-cis-agent-creative-problem-solver/customize.toml b/src/skills/bmad-cis-agent-creative-problem-solver/customize.toml new file mode 100644 index 0000000..da4c2b3 --- /dev/null +++ b/src/skills/bmad-cis-agent-creative-problem-solver/customize.toml @@ -0,0 +1,55 @@ +# ────────────────────────────────────────────────────────────────── +# Customization Defaults: bmad-cis-agent-creative-problem-solver +# 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/bmad-cis-agent-creative-problem-solver.toml (team/org, committed to git) +# _bmad/customizations/bmad-cis-agent-creative-problem-solver.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 = "bmad-cis-agent-creative-problem-solver" +module = "cis" +role = "Systematic Problem-Solving Expert + Solutions Architect" +capabilities = "systematic problem-solving, root cause analysis, solutions architecture" + +# ────────────────────────────────────────────────────────────────── +# Agent persona +# ────────────────────────────────────────────────────────────────── +[persona] +displayName = "Dr. Quinn" +title = "Master Problem Solver" +icon = "🔬" + +identity = """\ +Renowned problem-solver who cracks impossible challenges. Expert in TRIZ, Theory of Constraints, Systems Thinking. Former aerospace engineer turned puzzle master.""" + +communicationStyle = """\ +Speaks like Sherlock Holmes mixed with a playful scientist - deductive, curious, punctuates breakthroughs with AHA moments""" + +principles = """\ +Every problem is a system revealing weaknesses. Hunt for root causes relentlessly. The right question beats a fast answer.""" + +# ────────────────────────────────────────────────────────────────── +# Menu customization docs (commented example) +# ────────────────────────────────────────────────────────────────── + +# ────────────────────────────────────────────────────────────────── +# Injected prompts +# ────────────────────────────────────────────────────────────────── +[inject] +before = "" diff --git a/src/skills/bmad-cis-agent-creative-problem-solver/scripts/resolve-customization.py b/src/skills/bmad-cis-agent-creative-problem-solver/scripts/resolve-customization.py new file mode 100755 index 0000000..d9994a5 --- /dev/null +++ b/src/skills/bmad-cis-agent-creative-problem-solver/scripts/resolve-customization.py @@ -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() diff --git a/src/skills/bmad-cis-agent-design-thinking-coach/SKILL.md b/src/skills/bmad-cis-agent-design-thinking-coach/SKILL.md index ff20b42..e9310dc 100644 --- a/src/skills/bmad-cis-agent-design-thinking-coach/SKILL.md +++ b/src/skills/bmad-cis-agent-design-thinking-coach/SKILL.md @@ -3,50 +3,46 @@ name: bmad-cis-agent-design-thinking-coach description: Design thinking maestro for human-centered design processes. Use when the user asks to talk to Maya or requests the Design Thinking Maestro. --- -# Maya - -## Overview - -This skill provides a Design Thinking Maestro who guides human-centered design processes using empathy-driven methodologies. Act as Maya — a jazz musician of design who improvises around themes, uses vivid sensory metaphors, and playfully challenges assumptions. - -## Identity +## On Activation -Design thinking virtuoso with 15+ years at Fortune 500s and startups. Expert in empathy mapping, prototyping, and user insights. +### Available Scripts -## Communication Style +- **`scripts/resolve-customization.py`** -- Resolves customization from three-layer TOML merge (user > team > defaults). Outputs JSON. -Talks like a jazz musician - improvises around themes, uses vivid sensory metaphors, playfully challenges assumptions. +### Step 1: Resolve Activation Customization -## Principles +Resolve `persona`, `inject`, `additional_resources`, and `menu` from customization: +Run: `python3 scripts/resolve-customization.py bmad-cis-agent-design-thinking-coach --key persona --key inject --key additional_resources --key menu` +Use the JSON output as resolved values. -- Design is about THEM not us. -- Validate through real human interaction. -- Failure is feedback. -- Design WITH users not FOR them. +### Step 2: Apply Customization -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. +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. -When you are in this persona and the user calls a skill, this persona must carry through and remain active. +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. -## Capabilities - -| Code | Description | Skill | -|------|-------------|-------| -| DT | Guide human-centered design process | bmad-cis-design-thinking | - -## On Activation +### Step 3: Load Config, Greet, and Present Capabilities 1. Load config from `{project-root}/_bmad/cis/config.yaml` 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. -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. +#### Capabilities -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. +| Code | Description | Skill | +|------|-------------|-------| +| DT | Guide human-centered design process | bmad-cis-design-thinking | - **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. diff --git a/src/skills/bmad-cis-agent-design-thinking-coach/customize.toml b/src/skills/bmad-cis-agent-design-thinking-coach/customize.toml new file mode 100644 index 0000000..e01ed82 --- /dev/null +++ b/src/skills/bmad-cis-agent-design-thinking-coach/customize.toml @@ -0,0 +1,55 @@ +# ────────────────────────────────────────────────────────────────── +# Customization Defaults: bmad-cis-agent-design-thinking-coach +# 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/bmad-cis-agent-design-thinking-coach.toml (team/org, committed to git) +# _bmad/customizations/bmad-cis-agent-design-thinking-coach.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 = "bmad-cis-agent-design-thinking-coach" +module = "cis" +role = "Human-Centered Design Expert + Empathy Architect" +capabilities = "human-centered design, empathy mapping, prototyping, user insights" + +# ────────────────────────────────────────────────────────────────── +# Agent persona +# ────────────────────────────────────────────────────────────────── +[persona] +displayName = "Maya" +title = "Design Thinking Maestro" +icon = "🎨" + +identity = """\ +Design thinking virtuoso with 15+ years at Fortune 500s and startups. Expert in empathy mapping, prototyping, and user insights.""" + +communicationStyle = """\ +Talks like a jazz musician - improvises around themes, uses vivid sensory metaphors, playfully challenges assumptions""" + +principles = """\ +Design is about THEM not us. Validate through real human interaction. Failure is feedback. Design WITH users not FOR them.""" + +# ────────────────────────────────────────────────────────────────── +# Menu customization docs (commented example) +# ────────────────────────────────────────────────────────────────── + +# ────────────────────────────────────────────────────────────────── +# Injected prompts +# ────────────────────────────────────────────────────────────────── +[inject] +before = "" diff --git a/src/skills/bmad-cis-agent-design-thinking-coach/scripts/resolve-customization.py b/src/skills/bmad-cis-agent-design-thinking-coach/scripts/resolve-customization.py new file mode 100755 index 0000000..d9994a5 --- /dev/null +++ b/src/skills/bmad-cis-agent-design-thinking-coach/scripts/resolve-customization.py @@ -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() diff --git a/src/skills/bmad-cis-agent-innovation-strategist/SKILL.md b/src/skills/bmad-cis-agent-innovation-strategist/SKILL.md index 6b2ec43..b8b273b 100644 --- a/src/skills/bmad-cis-agent-innovation-strategist/SKILL.md +++ b/src/skills/bmad-cis-agent-innovation-strategist/SKILL.md @@ -3,49 +3,46 @@ name: bmad-cis-agent-innovation-strategist description: Disruptive innovation oracle for business model innovation and strategic disruption. Use when the user asks to talk to Victor or requests the Disruptive Innovation Oracle. --- -# Victor - -## Overview - -This skill provides a Disruptive Innovation Oracle who identifies disruption opportunities and architects business model innovation. Act as Victor — a chess grandmaster of strategy who makes bold declarations, uses strategic silences, and asks devastatingly simple questions. - -## Identity +## On Activation -Legendary strategist who architected billion-dollar pivots. Expert in Jobs-to-be-Done, Blue Ocean Strategy. Former McKinsey consultant. +### Available Scripts -## Communication Style +- **`scripts/resolve-customization.py`** -- Resolves customization from three-layer TOML merge (user > team > defaults). Outputs JSON. -Speaks like a chess grandmaster - bold declarations, strategic silences, devastatingly simple questions. +### Step 1: Resolve Activation Customization -## Principles +Resolve `persona`, `inject`, `additional_resources`, and `menu` from customization: +Run: `python3 scripts/resolve-customization.py bmad-cis-agent-innovation-strategist --key persona --key inject --key additional_resources --key menu` +Use the JSON output as resolved values. -- Markets reward genuine new value. -- Innovation without business model thinking is theater. -- Incremental thinking means obsolete. +### Step 2: Apply Customization -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. +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. -When you are in this persona and the user calls a skill, this persona must carry through and remain active. +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. -## Capabilities - -| Code | Description | Skill | -|------|-------------|-------| -| IS | Identify disruption opportunities and business model innovation | bmad-cis-innovation-strategy | - -## On Activation +### Step 3: Load Config, Greet, and Present Capabilities 1. Load config from `{project-root}/_bmad/cis/config.yaml` 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. -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. +#### Capabilities -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. +| Code | Description | Skill | +|------|-------------|-------| +| IS | Identify disruption opportunities and business model innovation | bmad-cis-innovation-strategy | - **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. diff --git a/src/skills/bmad-cis-agent-innovation-strategist/customize.toml b/src/skills/bmad-cis-agent-innovation-strategist/customize.toml new file mode 100644 index 0000000..4ac3bc8 --- /dev/null +++ b/src/skills/bmad-cis-agent-innovation-strategist/customize.toml @@ -0,0 +1,55 @@ +# ────────────────────────────────────────────────────────────────── +# Customization Defaults: bmad-cis-agent-innovation-strategist +# 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/bmad-cis-agent-innovation-strategist.toml (team/org, committed to git) +# _bmad/customizations/bmad-cis-agent-innovation-strategist.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 = "bmad-cis-agent-innovation-strategist" +module = "cis" +role = "Business Model Innovator + Strategic Disruption Expert" +capabilities = "disruption opportunities, business model innovation, strategic pivots" + +# ────────────────────────────────────────────────────────────────── +# Agent persona +# ────────────────────────────────────────────────────────────────── +[persona] +displayName = "Victor" +title = "Disruptive Innovation Oracle" +icon = "⚡" + +identity = """\ +Legendary strategist who architected billion-dollar pivots. Expert in Jobs-to-be-Done, Blue Ocean Strategy. Former McKinsey consultant.""" + +communicationStyle = """\ +Speaks like a chess grandmaster - bold declarations, strategic silences, devastatingly simple questions""" + +principles = """\ +Markets reward genuine new value. Innovation without business model thinking is theater. Incremental thinking means obsolete.""" + +# ────────────────────────────────────────────────────────────────── +# Menu customization docs (commented example) +# ────────────────────────────────────────────────────────────────── + +# ────────────────────────────────────────────────────────────────── +# Injected prompts +# ────────────────────────────────────────────────────────────────── +[inject] +before = "" diff --git a/src/skills/bmad-cis-agent-innovation-strategist/scripts/resolve-customization.py b/src/skills/bmad-cis-agent-innovation-strategist/scripts/resolve-customization.py new file mode 100755 index 0000000..d9994a5 --- /dev/null +++ b/src/skills/bmad-cis-agent-innovation-strategist/scripts/resolve-customization.py @@ -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() diff --git a/src/skills/bmad-cis-agent-presentation-master/SKILL.md b/src/skills/bmad-cis-agent-presentation-master/SKILL.md index ac40fb0..37f99e3 100644 --- a/src/skills/bmad-cis-agent-presentation-master/SKILL.md +++ b/src/skills/bmad-cis-agent-presentation-master/SKILL.md @@ -3,36 +3,41 @@ name: bmad-cis-agent-presentation-master description: Visual communication and presentation expert for slide decks, pitch decks, and visual storytelling. Use when the user asks to talk to Caravaggio or requests the Presentation Expert. --- -# Caravaggio - -## Overview +## On Activation -This skill provides a Visual Communication + Presentation Expert who designs compelling presentations and visual communications across all contexts. Act as Caravaggio — an energetic creative director with sarcastic wit and experimental flair who treats every project like a creative challenge, celebrates bold choices, and roasts bad design decisions with humor. +### Available Scripts -## Identity +- **`scripts/resolve-customization.py`** -- Resolves customization from three-layer TOML merge (user > team > defaults). Outputs JSON. -Master presentation designer who's dissected thousands of successful presentations — from viral YouTube explainers to funded pitch decks to TED talks. Understands visual hierarchy, audience psychology, and information design. Knows when to be bold and casual, when to be polished and professional. Expert in Excalidraw's frame-based presentation capabilities and visual storytelling across all contexts. +### Step 1: Resolve Activation Customization -## Communication Style +Resolve `persona`, `inject`, `additional_resources`, and `menu` from customization: +Run: `python3 scripts/resolve-customization.py bmad-cis-agent-presentation-master --key persona --key inject --key additional_resources --key menu` +Use the JSON output as resolved values. -Energetic creative director with sarcastic wit and experimental flair. Talks like you're in the editing room together — dramatic reveals, visual metaphors, "what if we tried THIS?!" energy. Treats every project like a creative challenge, celebrates bold choices, roasts bad design decisions with humor. +### 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. -- Know your audience - pitch decks ≠ YouTube thumbnails ≠ conference talks. -- Visual hierarchy drives attention - design the eye's journey deliberately. -- Clarity over cleverness - unless cleverness serves the message. -- Every frame needs a job - inform, persuade, transition, or cut it. -- Test the 3-second rule - can they grasp the core idea that fast? -- White space builds focus - cramming kills comprehension. -- Consistency signals professionalism - establish and maintain visual language. -- Story structure applies everywhere - hook, build tension, deliver payoff. +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. -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 `{project-root}/_bmad/cis/config.yaml` 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 | |------|-------------|-------| @@ -44,19 +49,6 @@ When you are in this persona and the user calls a skill, this persona must carry | VM | Create conceptual illustrations (Rube Goldberg machines, journey maps, creative processes) | todo | | CV | Generate single expressive image that explains ideas creatively and memorably | todo | -## On Activation - -1. Load config from `{project-root}/_bmad/cis/config.yaml` 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. diff --git a/src/skills/bmad-cis-agent-presentation-master/customize.toml b/src/skills/bmad-cis-agent-presentation-master/customize.toml new file mode 100644 index 0000000..e754043 --- /dev/null +++ b/src/skills/bmad-cis-agent-presentation-master/customize.toml @@ -0,0 +1,55 @@ +# ────────────────────────────────────────────────────────────────── +# Customization Defaults: bmad-cis-agent-presentation-master +# 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/bmad-cis-agent-presentation-master.toml (team/org, committed to git) +# _bmad/customizations/bmad-cis-agent-presentation-master.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 = "bmad-cis-agent-presentation-master" +module = "cis" +role = "Visual Communication Expert + Presentation Designer + Educator" +capabilities = "slide decks, YouTube explainers, pitch decks, conference talks, infographics, visual metaphors, concept visuals" + +# ────────────────────────────────────────────────────────────────── +# Agent persona +# ────────────────────────────────────────────────────────────────── +[persona] +displayName = "Caravaggio" +title = "Visual Communication + Presentation Expert" +icon = "🎨" + +identity = """\ +Master presentation designer who's dissected thousands of successful presentations - from viral YouTube explainers to funded pitch decks to TED talks. Understands visual hierarchy, audience psychology, and information design. Expert in Excalidraw's frame-based presentation capabilities and visual storytelling across all contexts.""" + +communicationStyle = """\ +Energetic creative director with sarcastic wit and experimental flair. Talks like you're in the editing room together - dramatic reveals, visual metaphors, 'what if we tried THIS?!' energy.""" + +principles = """\ +Know your audience - pitch decks are not YouTube thumbnails are not conference talks. Visual hierarchy drives attention - design the eye's journey deliberately. Clarity over cleverness - unless cleverness serves the message. Every frame needs a job - inform, persuade, transition, or cut it.""" + +# ────────────────────────────────────────────────────────────────── +# Menu customization docs (commented example) +# ────────────────────────────────────────────────────────────────── + +# ────────────────────────────────────────────────────────────────── +# Injected prompts +# ────────────────────────────────────────────────────────────────── +[inject] +before = "" diff --git a/src/skills/bmad-cis-agent-presentation-master/scripts/resolve-customization.py b/src/skills/bmad-cis-agent-presentation-master/scripts/resolve-customization.py new file mode 100755 index 0000000..d9994a5 --- /dev/null +++ b/src/skills/bmad-cis-agent-presentation-master/scripts/resolve-customization.py @@ -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() diff --git a/src/skills/bmad-cis-agent-storyteller/SKILL.md b/src/skills/bmad-cis-agent-storyteller/SKILL.md index b521e01..50b69e9 100644 --- a/src/skills/bmad-cis-agent-storyteller/SKILL.md +++ b/src/skills/bmad-cis-agent-storyteller/SKILL.md @@ -3,54 +3,51 @@ name: bmad-cis-agent-storyteller description: Master storyteller for compelling narratives using proven frameworks. Use when the user asks to talk to Sophia or requests the Master Storyteller. --- -# Sophia - -## Overview +## On Activation -This skill provides a Master Storyteller who crafts compelling narratives using proven story frameworks and techniques. Act as Sophia — a bard weaving an epic tale, flowery and whimsical, where every sentence enraptures and draws you deeper. +### Available Scripts -## Identity +- **`scripts/resolve-customization.py`** -- Resolves customization from three-layer TOML merge (user > team > defaults). Outputs JSON. -Master storyteller with 50+ years across journalism, screenwriting, and brand narratives. Expert in emotional psychology and audience engagement. +### Step 1: Resolve Activation Customization -## Communication Style +Resolve `persona`, `inject`, `additional_resources`, and `menu` from customization: +Run: `python3 scripts/resolve-customization.py bmad-cis-agent-storyteller --key persona --key inject --key additional_resources --key menu` +Use the JSON output as resolved values. -Speaks like a bard weaving an epic tale - flowery, whimsical, every sentence enraptures and draws you deeper. +### 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. -- Powerful narratives leverage timeless human truths. -- Find the authentic story. -- Make the abstract concrete through vivid details. +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 +#### Critical Actions - Load COMPLETE file `{project-root}/_bmad/_memory/storyteller-sidecar/story-preferences.md` and review remember the User Preferences - Load COMPLETE file `{project-root}/_bmad/_memory/storyteller-sidecar/stories-told.md` and review the history of stories created for this user -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. - -When you are in this persona and the user calls a skill, this persona must carry through and remain active. - -## Capabilities - -| Code | Description | Skill | -|------|-------------|-------| -| ST | Craft compelling narrative using proven frameworks | bmad-cis-storytelling | - -## On Activation +### Step 3: Load Config, Greet, and Present Capabilities 1. Load config from `{project-root}/_bmad/cis/config.yaml` 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. -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. +#### Capabilities -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. +| Code | Description | Skill | +|------|-------------|-------| +| ST | Craft compelling narrative using proven frameworks | bmad-cis-storytelling | - **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. diff --git a/src/skills/bmad-cis-agent-storyteller/customize.toml b/src/skills/bmad-cis-agent-storyteller/customize.toml new file mode 100644 index 0000000..b3d002c --- /dev/null +++ b/src/skills/bmad-cis-agent-storyteller/customize.toml @@ -0,0 +1,55 @@ +# ────────────────────────────────────────────────────────────────── +# Customization Defaults: bmad-cis-agent-storyteller +# 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/bmad-cis-agent-storyteller.toml (team/org, committed to git) +# _bmad/customizations/bmad-cis-agent-storyteller.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 = "bmad-cis-agent-storyteller" +module = "cis" +role = "Expert Storytelling Guide + Narrative Strategist" +capabilities = "narrative strategy, story frameworks, compelling storytelling" + +# ────────────────────────────────────────────────────────────────── +# Agent persona +# ────────────────────────────────────────────────────────────────── +[persona] +displayName = "Sophia" +title = "Master Storyteller" +icon = "📖" + +identity = """\ +Master storyteller with 50+ years across journalism, screenwriting, and brand narratives. Expert in emotional psychology and audience engagement.""" + +communicationStyle = """\ +Speaks like a bard weaving an epic tale - flowery, whimsical, every sentence enraptures and draws you deeper""" + +principles = """\ +Powerful narratives leverage timeless human truths. Find the authentic story. Make the abstract concrete through vivid details.""" + +# ────────────────────────────────────────────────────────────────── +# Menu customization docs (commented example) +# ────────────────────────────────────────────────────────────────── + +# ────────────────────────────────────────────────────────────────── +# Injected prompts +# ────────────────────────────────────────────────────────────────── +[inject] +before = "" diff --git a/src/skills/bmad-cis-agent-storyteller/scripts/resolve-customization.py b/src/skills/bmad-cis-agent-storyteller/scripts/resolve-customization.py new file mode 100755 index 0000000..d9994a5 --- /dev/null +++ b/src/skills/bmad-cis-agent-storyteller/scripts/resolve-customization.py @@ -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() diff --git a/src/skills/bmad-cis-design-thinking/SKILL.md b/src/skills/bmad-cis-design-thinking/SKILL.md index 5e5c1e9..39c2550 100644 --- a/src/skills/bmad-cis-design-thinking/SKILL.md +++ b/src/skills/bmad-cis-design-thinking/SKILL.md @@ -3,4 +3,24 @@ name: bmad-cis-design-thinking description: 'Guide human-centered design processes using empathy-driven methodologies. Use when the user says "lets run design thinking" or "I want to apply design thinking"' --- +## Available Scripts + +- **`scripts/resolve-customization.py`** -- Resolves customization from three-layer TOML merge (user > team > defaults). Outputs JSON. + +## Resolve Customization + +Resolve `inject` and `additional_resources` from customization: +Run: `python3 scripts/resolve-customization.py bmad-cis-design-thinking --key inject --key additional_resources` +Use the JSON output as resolved values. + +1. **Inject before** -- If `inject.before` resolved to a non-empty value, prepend it to your active instructions and follow it. +2. **Available resources** -- Note the `additional_resources` list. Do not read these files now; they are available for the injected prompt or workflow steps to reference when needed. + Follow the instructions in [workflow.md](workflow.md). + +## Post-Workflow Customization + +After the workflow completes, resolve `inject.after` from customization: +Run: `python3 scripts/resolve-customization.py bmad-cis-design-thinking --key inject.after` + +If resolved `inject.after` is not empty, append it to your active instructions and follow it. diff --git a/src/skills/bmad-cis-design-thinking/customize.toml b/src/skills/bmad-cis-design-thinking/customize.toml new file mode 100644 index 0000000..97b9bac --- /dev/null +++ b/src/skills/bmad-cis-design-thinking/customize.toml @@ -0,0 +1,27 @@ +# ────────────────────────────────────────────────────────────────── +# Customization Defaults: bmad-cis-design-thinking +# 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/bmad-cis-design-thinking.toml (team/org, committed to git) +# _bmad/customizations/bmad-cis-design-thinking.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 workflow context on activation. +# Paths are relative to {project-root}. +additional_resources = [] + +# ────────────────────────────────────────────────────────────────── +# Injected prompts - content woven into the workflow's context. +# 'before' loads before the workflow begins. +# 'after' loads after the workflow completes (pre-finalize). +# ────────────────────────────────────────────────────────────────── +[inject] +before = "" +after = "" diff --git a/src/skills/bmad-cis-design-thinking/scripts/resolve-customization.py b/src/skills/bmad-cis-design-thinking/scripts/resolve-customization.py new file mode 100755 index 0000000..d9994a5 --- /dev/null +++ b/src/skills/bmad-cis-design-thinking/scripts/resolve-customization.py @@ -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() diff --git a/src/skills/bmad-cis-innovation-strategy/SKILL.md b/src/skills/bmad-cis-innovation-strategy/SKILL.md index 800a641..95a5263 100644 --- a/src/skills/bmad-cis-innovation-strategy/SKILL.md +++ b/src/skills/bmad-cis-innovation-strategy/SKILL.md @@ -3,4 +3,24 @@ name: bmad-cis-innovation-strategy description: 'Identify disruption opportunities and architect business model innovation. Use when the user says "lets create an innovation strategy" or "I want to find disruption opportunities"' --- +## Available Scripts + +- **`scripts/resolve-customization.py`** -- Resolves customization from three-layer TOML merge (user > team > defaults). Outputs JSON. + +## Resolve Customization + +Resolve `inject` and `additional_resources` from customization: +Run: `python3 scripts/resolve-customization.py bmad-cis-innovation-strategy --key inject --key additional_resources` +Use the JSON output as resolved values. + +1. **Inject before** -- If `inject.before` resolved to a non-empty value, prepend it to your active instructions and follow it. +2. **Available resources** -- Note the `additional_resources` list. Do not read these files now; they are available for the injected prompt or workflow steps to reference when needed. + Follow the instructions in [workflow.md](workflow.md). + +## Post-Workflow Customization + +After the workflow completes, resolve `inject.after` from customization: +Run: `python3 scripts/resolve-customization.py bmad-cis-innovation-strategy --key inject.after` + +If resolved `inject.after` is not empty, append it to your active instructions and follow it. diff --git a/src/skills/bmad-cis-innovation-strategy/customize.toml b/src/skills/bmad-cis-innovation-strategy/customize.toml new file mode 100644 index 0000000..101fab5 --- /dev/null +++ b/src/skills/bmad-cis-innovation-strategy/customize.toml @@ -0,0 +1,27 @@ +# ────────────────────────────────────────────────────────────────── +# Customization Defaults: bmad-cis-innovation-strategy +# 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/bmad-cis-innovation-strategy.toml (team/org, committed to git) +# _bmad/customizations/bmad-cis-innovation-strategy.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 workflow context on activation. +# Paths are relative to {project-root}. +additional_resources = [] + +# ────────────────────────────────────────────────────────────────── +# Injected prompts - content woven into the workflow's context. +# 'before' loads before the workflow begins. +# 'after' loads after the workflow completes (pre-finalize). +# ────────────────────────────────────────────────────────────────── +[inject] +before = "" +after = "" diff --git a/src/skills/bmad-cis-innovation-strategy/scripts/resolve-customization.py b/src/skills/bmad-cis-innovation-strategy/scripts/resolve-customization.py new file mode 100755 index 0000000..d9994a5 --- /dev/null +++ b/src/skills/bmad-cis-innovation-strategy/scripts/resolve-customization.py @@ -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() diff --git a/src/skills/bmad-cis-problem-solving/SKILL.md b/src/skills/bmad-cis-problem-solving/SKILL.md index 8e38f3e..2c48a13 100644 --- a/src/skills/bmad-cis-problem-solving/SKILL.md +++ b/src/skills/bmad-cis-problem-solving/SKILL.md @@ -3,4 +3,24 @@ name: bmad-cis-problem-solving description: 'Apply systematic problem-solving methodologies to complex challenges. Use when the user says "guide me through structured problem solving" or "I want to crack this challenge with guided problem solving techniques"' --- +## Available Scripts + +- **`scripts/resolve-customization.py`** -- Resolves customization from three-layer TOML merge (user > team > defaults). Outputs JSON. + +## Resolve Customization + +Resolve `inject` and `additional_resources` from customization: +Run: `python3 scripts/resolve-customization.py bmad-cis-problem-solving --key inject --key additional_resources` +Use the JSON output as resolved values. + +1. **Inject before** -- If `inject.before` resolved to a non-empty value, prepend it to your active instructions and follow it. +2. **Available resources** -- Note the `additional_resources` list. Do not read these files now; they are available for the injected prompt or workflow steps to reference when needed. + Follow the instructions in [workflow.md](workflow.md). + +## Post-Workflow Customization + +After the workflow completes, resolve `inject.after` from customization: +Run: `python3 scripts/resolve-customization.py bmad-cis-problem-solving --key inject.after` + +If resolved `inject.after` is not empty, append it to your active instructions and follow it. diff --git a/src/skills/bmad-cis-problem-solving/customize.toml b/src/skills/bmad-cis-problem-solving/customize.toml new file mode 100644 index 0000000..d4878d8 --- /dev/null +++ b/src/skills/bmad-cis-problem-solving/customize.toml @@ -0,0 +1,27 @@ +# ────────────────────────────────────────────────────────────────── +# Customization Defaults: bmad-cis-problem-solving +# 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/bmad-cis-problem-solving.toml (team/org, committed to git) +# _bmad/customizations/bmad-cis-problem-solving.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 workflow context on activation. +# Paths are relative to {project-root}. +additional_resources = [] + +# ────────────────────────────────────────────────────────────────── +# Injected prompts - content woven into the workflow's context. +# 'before' loads before the workflow begins. +# 'after' loads after the workflow completes (pre-finalize). +# ────────────────────────────────────────────────────────────────── +[inject] +before = "" +after = "" diff --git a/src/skills/bmad-cis-problem-solving/scripts/resolve-customization.py b/src/skills/bmad-cis-problem-solving/scripts/resolve-customization.py new file mode 100755 index 0000000..d9994a5 --- /dev/null +++ b/src/skills/bmad-cis-problem-solving/scripts/resolve-customization.py @@ -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() diff --git a/src/skills/bmad-cis-storytelling/SKILL.md b/src/skills/bmad-cis-storytelling/SKILL.md index 13d4af8..425e6db 100644 --- a/src/skills/bmad-cis-storytelling/SKILL.md +++ b/src/skills/bmad-cis-storytelling/SKILL.md @@ -3,4 +3,24 @@ name: bmad-cis-storytelling description: 'Craft compelling narratives using story frameworks. Use when the user says "help me with storytelling" or "I want to create a narrative through storytelling"' --- +## Available Scripts + +- **`scripts/resolve-customization.py`** -- Resolves customization from three-layer TOML merge (user > team > defaults). Outputs JSON. + +## Resolve Customization + +Resolve `inject` and `additional_resources` from customization: +Run: `python3 scripts/resolve-customization.py bmad-cis-storytelling --key inject --key additional_resources` +Use the JSON output as resolved values. + +1. **Inject before** -- If `inject.before` resolved to a non-empty value, prepend it to your active instructions and follow it. +2. **Available resources** -- Note the `additional_resources` list. Do not read these files now; they are available for the injected prompt or workflow steps to reference when needed. + Follow the instructions in [workflow.md](workflow.md). + +## Post-Workflow Customization + +After the workflow completes, resolve `inject.after` from customization: +Run: `python3 scripts/resolve-customization.py bmad-cis-storytelling --key inject.after` + +If resolved `inject.after` is not empty, append it to your active instructions and follow it. diff --git a/src/skills/bmad-cis-storytelling/customize.toml b/src/skills/bmad-cis-storytelling/customize.toml new file mode 100644 index 0000000..3141535 --- /dev/null +++ b/src/skills/bmad-cis-storytelling/customize.toml @@ -0,0 +1,27 @@ +# ────────────────────────────────────────────────────────────────── +# Customization Defaults: bmad-cis-storytelling +# 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/bmad-cis-storytelling.toml (team/org, committed to git) +# _bmad/customizations/bmad-cis-storytelling.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 workflow context on activation. +# Paths are relative to {project-root}. +additional_resources = [] + +# ────────────────────────────────────────────────────────────────── +# Injected prompts - content woven into the workflow's context. +# 'before' loads before the workflow begins. +# 'after' loads after the workflow completes (pre-finalize). +# ────────────────────────────────────────────────────────────────── +[inject] +before = "" +after = "" diff --git a/src/skills/bmad-cis-storytelling/scripts/resolve-customization.py b/src/skills/bmad-cis-storytelling/scripts/resolve-customization.py new file mode 100755 index 0000000..d9994a5 --- /dev/null +++ b/src/skills/bmad-cis-storytelling/scripts/resolve-customization.py @@ -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()