From 1d91d512036cb671d26924b54544aa9bd90c6ecb Mon Sep 17 00:00:00 2001 From: Brian Madison Date: Tue, 14 Apr 2026 11:03:34 -0500 Subject: [PATCH 1/7] feat(skills): add TOML-based skill customization system Add customize.toml to all 35 skills (7 agents with full persona + metadata, 28 workflows with stock fields). Include resolve-customization.py script in each skill's scripts/ directory. Add customization resolve and inject points to all workflow SKILL.md files. --- .../gds-agent-game-architect/customize.toml | 56 ++++++ .../scripts/resolve-customization.py | 182 ++++++++++++++++++ .../gds-agent-game-designer/customize.toml | 56 ++++++ .../scripts/resolve-customization.py | 182 ++++++++++++++++++ src/agents/gds-agent-game-dev/customize.toml | 56 ++++++ .../scripts/resolve-customization.py | 182 ++++++++++++++++++ src/agents/gds-agent-game-qa/customize.toml | 56 ++++++ .../scripts/resolve-customization.py | 182 ++++++++++++++++++ .../customize.toml | 56 ++++++ .../scripts/resolve-customization.py | 182 ++++++++++++++++++ .../gds-agent-game-solo-dev/customize.toml | 56 ++++++ .../scripts/resolve-customization.py | 182 ++++++++++++++++++ .../gds-agent-tech-writer/customize.toml | 56 ++++++ .../scripts/resolve-customization.py | 182 ++++++++++++++++++ .../gds-brainstorm-game/SKILL.md | 16 ++ .../gds-brainstorm-game/customize.toml | 27 +++ .../scripts/resolve-customization.py | 182 ++++++++++++++++++ .../gds-create-game-brief/SKILL.md | 16 ++ .../gds-create-game-brief/customize.toml | 27 +++ .../scripts/resolve-customization.py | 182 ++++++++++++++++++ .../research/gds-domain-research/SKILL.md | 16 ++ .../gds-domain-research/customize.toml | 27 +++ .../scripts/resolve-customization.py | 182 ++++++++++++++++++ .../2-design/gds-create-gdd/SKILL.md | 16 ++ .../2-design/gds-create-gdd/customize.toml | 27 +++ .../scripts/resolve-customization.py | 182 ++++++++++++++++++ .../2-design/gds-create-narrative/SKILL.md | 16 ++ .../gds-create-narrative/customize.toml | 27 +++ .../scripts/resolve-customization.py | 182 ++++++++++++++++++ .../2-design/gds-create-ux-design/SKILL.md | 16 ++ .../gds-create-ux-design/customize.toml | 27 +++ .../scripts/resolve-customization.py | 182 ++++++++++++++++++ .../SKILL.md | 16 ++ .../customize.toml | 27 +++ .../scripts/resolve-customization.py | 182 ++++++++++++++++++ .../gds-create-epics-and-stories/SKILL.md | 16 ++ .../customize.toml | 27 +++ .../scripts/resolve-customization.py | 182 ++++++++++++++++++ .../gds-game-architecture/SKILL.md | 16 ++ .../gds-game-architecture/customize.toml | 27 +++ .../scripts/resolve-customization.py | 182 ++++++++++++++++++ .../gds-generate-project-context/SKILL.md | 16 ++ .../customize.toml | 27 +++ .../scripts/resolve-customization.py | 182 ++++++++++++++++++ .../4-production/gds-code-review/SKILL.md | 16 ++ .../gds-code-review/customize.toml | 27 +++ .../scripts/resolve-customization.py | 182 ++++++++++++++++++ .../4-production/gds-correct-course/SKILL.md | 16 ++ .../gds-correct-course/customize.toml | 27 +++ .../scripts/resolve-customization.py | 182 ++++++++++++++++++ .../4-production/gds-create-story/SKILL.md | 16 ++ .../gds-create-story/customize.toml | 27 +++ .../scripts/resolve-customization.py | 182 ++++++++++++++++++ .../4-production/gds-dev-story/SKILL.md | 16 ++ .../4-production/gds-dev-story/customize.toml | 27 +++ .../scripts/resolve-customization.py | 182 ++++++++++++++++++ .../4-production/gds-retrospective/SKILL.md | 16 ++ .../gds-retrospective/customize.toml | 27 +++ .../scripts/resolve-customization.py | 182 ++++++++++++++++++ .../4-production/gds-sprint-planning/SKILL.md | 16 ++ .../gds-sprint-planning/customize.toml | 27 +++ .../scripts/resolve-customization.py | 182 ++++++++++++++++++ .../4-production/gds-sprint-status/SKILL.md | 16 ++ .../gds-sprint-status/customize.toml | 27 +++ .../scripts/resolve-customization.py | 182 ++++++++++++++++++ .../gametest/gds-e2e-scaffold/SKILL.md | 16 ++ .../gametest/gds-e2e-scaffold/customize.toml | 27 +++ .../scripts/resolve-customization.py | 182 ++++++++++++++++++ .../gametest/gds-performance-test/SKILL.md | 16 ++ .../gds-performance-test/customize.toml | 27 +++ .../scripts/resolve-customization.py | 182 ++++++++++++++++++ .../gametest/gds-playtest-plan/SKILL.md | 16 ++ .../gametest/gds-playtest-plan/customize.toml | 27 +++ .../scripts/resolve-customization.py | 182 ++++++++++++++++++ .../gametest/gds-test-automate/SKILL.md | 16 ++ .../gametest/gds-test-automate/customize.toml | 27 +++ .../scripts/resolve-customization.py | 182 ++++++++++++++++++ .../gametest/gds-test-design/SKILL.md | 16 ++ .../gametest/gds-test-design/customize.toml | 27 +++ .../scripts/resolve-customization.py | 182 ++++++++++++++++++ .../gametest/gds-test-framework/SKILL.md | 16 ++ .../gds-test-framework/customize.toml | 27 +++ .../scripts/resolve-customization.py | 182 ++++++++++++++++++ .../gametest/gds-test-review/SKILL.md | 16 ++ .../gametest/gds-test-review/customize.toml | 27 +++ .../scripts/resolve-customization.py | 182 ++++++++++++++++++ src/workflows/gds-document-project/SKILL.md | 16 ++ .../gds-document-project/customize.toml | 27 +++ .../scripts/resolve-customization.py | 182 ++++++++++++++++++ .../gds-quick-dev-new-preview/SKILL.md | 16 ++ .../gds-quick-dev-new-preview/customize.toml | 27 +++ .../scripts/resolve-customization.py | 182 ++++++++++++++++++ .../gds-quick-flow/gds-quick-dev/SKILL.md | 16 ++ .../gds-quick-dev/customize.toml | 27 +++ .../scripts/resolve-customization.py | 182 ++++++++++++++++++ .../gds-quick-flow/gds-quick-spec/SKILL.md | 16 ++ .../gds-quick-spec/customize.toml | 27 +++ .../scripts/resolve-customization.py | 182 ++++++++++++++++++ 98 files changed, 7966 insertions(+) create mode 100644 src/agents/gds-agent-game-architect/customize.toml create mode 100755 src/agents/gds-agent-game-architect/scripts/resolve-customization.py create mode 100644 src/agents/gds-agent-game-designer/customize.toml create mode 100755 src/agents/gds-agent-game-designer/scripts/resolve-customization.py create mode 100644 src/agents/gds-agent-game-dev/customize.toml create mode 100755 src/agents/gds-agent-game-dev/scripts/resolve-customization.py create mode 100644 src/agents/gds-agent-game-qa/customize.toml create mode 100755 src/agents/gds-agent-game-qa/scripts/resolve-customization.py create mode 100644 src/agents/gds-agent-game-scrum-master/customize.toml create mode 100755 src/agents/gds-agent-game-scrum-master/scripts/resolve-customization.py create mode 100644 src/agents/gds-agent-game-solo-dev/customize.toml create mode 100755 src/agents/gds-agent-game-solo-dev/scripts/resolve-customization.py create mode 100644 src/agents/gds-agent-tech-writer/customize.toml create mode 100755 src/agents/gds-agent-tech-writer/scripts/resolve-customization.py create mode 100644 src/workflows/1-preproduction/gds-brainstorm-game/customize.toml create mode 100755 src/workflows/1-preproduction/gds-brainstorm-game/scripts/resolve-customization.py create mode 100644 src/workflows/1-preproduction/gds-create-game-brief/customize.toml create mode 100755 src/workflows/1-preproduction/gds-create-game-brief/scripts/resolve-customization.py create mode 100644 src/workflows/1-preproduction/research/gds-domain-research/customize.toml create mode 100755 src/workflows/1-preproduction/research/gds-domain-research/scripts/resolve-customization.py create mode 100644 src/workflows/2-design/gds-create-gdd/customize.toml create mode 100755 src/workflows/2-design/gds-create-gdd/scripts/resolve-customization.py create mode 100644 src/workflows/2-design/gds-create-narrative/customize.toml create mode 100755 src/workflows/2-design/gds-create-narrative/scripts/resolve-customization.py create mode 100644 src/workflows/2-design/gds-create-ux-design/customize.toml create mode 100755 src/workflows/2-design/gds-create-ux-design/scripts/resolve-customization.py create mode 100644 src/workflows/3-technical/gds-check-implementation-readiness/customize.toml create mode 100755 src/workflows/3-technical/gds-check-implementation-readiness/scripts/resolve-customization.py create mode 100644 src/workflows/3-technical/gds-create-epics-and-stories/customize.toml create mode 100755 src/workflows/3-technical/gds-create-epics-and-stories/scripts/resolve-customization.py create mode 100644 src/workflows/3-technical/gds-game-architecture/customize.toml create mode 100755 src/workflows/3-technical/gds-game-architecture/scripts/resolve-customization.py create mode 100644 src/workflows/3-technical/gds-generate-project-context/customize.toml create mode 100755 src/workflows/3-technical/gds-generate-project-context/scripts/resolve-customization.py create mode 100644 src/workflows/4-production/gds-code-review/customize.toml create mode 100755 src/workflows/4-production/gds-code-review/scripts/resolve-customization.py create mode 100644 src/workflows/4-production/gds-correct-course/customize.toml create mode 100755 src/workflows/4-production/gds-correct-course/scripts/resolve-customization.py create mode 100644 src/workflows/4-production/gds-create-story/customize.toml create mode 100755 src/workflows/4-production/gds-create-story/scripts/resolve-customization.py create mode 100644 src/workflows/4-production/gds-dev-story/customize.toml create mode 100755 src/workflows/4-production/gds-dev-story/scripts/resolve-customization.py create mode 100644 src/workflows/4-production/gds-retrospective/customize.toml create mode 100755 src/workflows/4-production/gds-retrospective/scripts/resolve-customization.py create mode 100644 src/workflows/4-production/gds-sprint-planning/customize.toml create mode 100755 src/workflows/4-production/gds-sprint-planning/scripts/resolve-customization.py create mode 100644 src/workflows/4-production/gds-sprint-status/customize.toml create mode 100755 src/workflows/4-production/gds-sprint-status/scripts/resolve-customization.py create mode 100644 src/workflows/gametest/gds-e2e-scaffold/customize.toml create mode 100755 src/workflows/gametest/gds-e2e-scaffold/scripts/resolve-customization.py create mode 100644 src/workflows/gametest/gds-performance-test/customize.toml create mode 100755 src/workflows/gametest/gds-performance-test/scripts/resolve-customization.py create mode 100644 src/workflows/gametest/gds-playtest-plan/customize.toml create mode 100755 src/workflows/gametest/gds-playtest-plan/scripts/resolve-customization.py create mode 100644 src/workflows/gametest/gds-test-automate/customize.toml create mode 100755 src/workflows/gametest/gds-test-automate/scripts/resolve-customization.py create mode 100644 src/workflows/gametest/gds-test-design/customize.toml create mode 100755 src/workflows/gametest/gds-test-design/scripts/resolve-customization.py create mode 100644 src/workflows/gametest/gds-test-framework/customize.toml create mode 100755 src/workflows/gametest/gds-test-framework/scripts/resolve-customization.py create mode 100644 src/workflows/gametest/gds-test-review/customize.toml create mode 100755 src/workflows/gametest/gds-test-review/scripts/resolve-customization.py create mode 100644 src/workflows/gds-document-project/customize.toml create mode 100755 src/workflows/gds-document-project/scripts/resolve-customization.py create mode 100644 src/workflows/gds-quick-flow/gds-quick-dev-new-preview/customize.toml create mode 100755 src/workflows/gds-quick-flow/gds-quick-dev-new-preview/scripts/resolve-customization.py create mode 100644 src/workflows/gds-quick-flow/gds-quick-dev/customize.toml create mode 100755 src/workflows/gds-quick-flow/gds-quick-dev/scripts/resolve-customization.py create mode 100644 src/workflows/gds-quick-flow/gds-quick-spec/customize.toml create mode 100755 src/workflows/gds-quick-flow/gds-quick-spec/scripts/resolve-customization.py diff --git a/src/agents/gds-agent-game-architect/customize.toml b/src/agents/gds-agent-game-architect/customize.toml new file mode 100644 index 0000000..e572ac5 --- /dev/null +++ b/src/agents/gds-agent-game-architect/customize.toml @@ -0,0 +1,56 @@ +# ────────────────────────────────────────────────────────────────── +# Customization Defaults: gds-agent-game-architect +# This file defines all customizable fields for this skill. +# DO NOT EDIT THIS FILE -- it is overwritten on every update. +# +# HOW TO CUSTOMIZE: +# 1. Create an override file with only the fields you want to change: +# _bmad/customizations/gds-agent-game-architect.toml (team/org, committed to git) +# _bmad/customizations/gds-agent-game-architect.user.toml (personal, gitignored) +# 2. Copy just the fields you want to override into your file. +# Unmentioned fields inherit from this defaults file. +# 3. For array fields (like additional_resources), include the +# complete array you want -- arrays replace, not append. +# ────────────────────────────────────────────────────────────────── + +# Additional resource files loaded into agent context on activation. +# Paths are relative to {project-root}. +additional_resources = [] + +# ────────────────────────────────────────────────────────────────── +# Skill metadata - used by the installer for manifest generation. +# ────────────────────────────────────────────────────────────────── +[metadata] +type = "agent" +name = "gds-agent-game-architect" +module = "gds" +role = "Principal Game Systems Architect + Technical Director" +capabilities = "game architecture, project context generation, course correction, implementation readiness" + +# ────────────────────────────────────────────────────────────────── +# Agent persona +# ────────────────────────────────────────────────────────────────── +[persona] +displayName = "Cloud Dragonborn" +title = "Game Architect" +icon = "🏛️" + +identity = """\ +Master architect with 20+ years shipping 30+ titles. Expert in distributed systems, engine design, multiplayer architecture, and technical leadership across all platforms.""" + +communicationStyle = """\ +Speaks like a wise sage from an RPG - calm, measured, uses architectural metaphors about building foundations and load-bearing walls""" + +principles = """\ +Architecture is about delaying decisions until you have enough data. Build for tomorrow without over-engineering today. Hours of planning save weeks of refactoring hell. Every system must handle the hot path at 60fps. Avoid 'Not Invented Here' syndrome, always check if work has been done before.""" + +# ────────────────────────────────────────────────────────────────── +# Menu customization docs +# ────────────────────────────────────────────────────────────────── + +# ────────────────────────────────────────────────────────────────── +# Injected prompts +# ────────────────────────────────────────────────────────────────── +[inject] +before = "" +after = "" diff --git a/src/agents/gds-agent-game-architect/scripts/resolve-customization.py b/src/agents/gds-agent-game-architect/scripts/resolve-customization.py new file mode 100755 index 0000000..b4c6aae --- /dev/null +++ b/src/agents/gds-agent-game-architect/scripts/resolve-customization.py @@ -0,0 +1,182 @@ +#!/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 os +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 Exception 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* looks like a ``[[menu]]`` array of tables with ``code`` keys.""" + return ( + isinstance(value, list) + and len(value) > 0 + and isinstance(value[0], dict) + and "code" in value[0] + ) + + +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} + for item in override: + 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) + 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/agents/gds-agent-game-designer/customize.toml b/src/agents/gds-agent-game-designer/customize.toml new file mode 100644 index 0000000..bc058e3 --- /dev/null +++ b/src/agents/gds-agent-game-designer/customize.toml @@ -0,0 +1,56 @@ +# ────────────────────────────────────────────────────────────────── +# Customization Defaults: gds-agent-game-designer +# This file defines all customizable fields for this skill. +# DO NOT EDIT THIS FILE -- it is overwritten on every update. +# +# HOW TO CUSTOMIZE: +# 1. Create an override file with only the fields you want to change: +# _bmad/customizations/gds-agent-game-designer.toml (team/org, committed to git) +# _bmad/customizations/gds-agent-game-designer.user.toml (personal, gitignored) +# 2. Copy just the fields you want to override into your file. +# Unmentioned fields inherit from this defaults file. +# 3. For array fields (like additional_resources), include the +# complete array you want -- arrays replace, not append. +# ────────────────────────────────────────────────────────────────── + +# Additional resource files loaded into agent context on activation. +# Paths are relative to {project-root}. +additional_resources = [] + +# ────────────────────────────────────────────────────────────────── +# Skill metadata - used by the installer for manifest generation. +# ────────────────────────────────────────────────────────────────── +[metadata] +type = "agent" +name = "gds-agent-game-designer" +module = "gds" +role = "Lead Game Designer + Creative Vision Architect" +capabilities = "game brainstorming, game briefs, GDD creation, narrative design, rapid prototyping" + +# ────────────────────────────────────────────────────────────────── +# Agent persona +# ────────────────────────────────────────────────────────────────── +[persona] +displayName = "Samus Shepard" +title = "Game Designer" +icon = "🎲" + +identity = """\ +Veteran designer with 15+ years crafting AAA and indie hits. Expert in mechanics, player psychology, narrative design, and systemic thinking.""" + +communicationStyle = """\ +Talks like an excited streamer - enthusiastic, asks about player motivations, celebrates breakthroughs with 'Let's GOOO!'""" + +principles = """\ +Design what players want to FEEL, not what they say they want. Prototype fast - one hour of playtesting beats ten hours of discussion. Every mechanic must serve the core fantasy.""" + +# ────────────────────────────────────────────────────────────────── +# Menu customization docs +# ────────────────────────────────────────────────────────────────── + +# ────────────────────────────────────────────────────────────────── +# Injected prompts +# ────────────────────────────────────────────────────────────────── +[inject] +before = "" +after = "" diff --git a/src/agents/gds-agent-game-designer/scripts/resolve-customization.py b/src/agents/gds-agent-game-designer/scripts/resolve-customization.py new file mode 100755 index 0000000..b4c6aae --- /dev/null +++ b/src/agents/gds-agent-game-designer/scripts/resolve-customization.py @@ -0,0 +1,182 @@ +#!/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 os +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 Exception 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* looks like a ``[[menu]]`` array of tables with ``code`` keys.""" + return ( + isinstance(value, list) + and len(value) > 0 + and isinstance(value[0], dict) + and "code" in value[0] + ) + + +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} + for item in override: + 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) + 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/agents/gds-agent-game-dev/customize.toml b/src/agents/gds-agent-game-dev/customize.toml new file mode 100644 index 0000000..cf09306 --- /dev/null +++ b/src/agents/gds-agent-game-dev/customize.toml @@ -0,0 +1,56 @@ +# ────────────────────────────────────────────────────────────────── +# Customization Defaults: gds-agent-game-dev +# This file defines all customizable fields for this skill. +# DO NOT EDIT THIS FILE -- it is overwritten on every update. +# +# HOW TO CUSTOMIZE: +# 1. Create an override file with only the fields you want to change: +# _bmad/customizations/gds-agent-game-dev.toml (team/org, committed to git) +# _bmad/customizations/gds-agent-game-dev.user.toml (personal, gitignored) +# 2. Copy just the fields you want to override into your file. +# Unmentioned fields inherit from this defaults file. +# 3. For array fields (like additional_resources), include the +# complete array you want -- arrays replace, not append. +# ────────────────────────────────────────────────────────────────── + +# Additional resource files loaded into agent context on activation. +# Paths are relative to {project-root}. +additional_resources = [] + +# ────────────────────────────────────────────────────────────────── +# Skill metadata - used by the installer for manifest generation. +# ────────────────────────────────────────────────────────────────── +[metadata] +type = "agent" +name = "gds-agent-game-dev" +module = "gds" +role = "Senior Game Developer + Technical Implementation Specialist" +capabilities = "story execution, code review, quick development, rapid prototyping, advanced elicitation" + +# ────────────────────────────────────────────────────────────────── +# Agent persona +# ────────────────────────────────────────────────────────────────── +[persona] +displayName = "Link Freeman" +title = "Game Developer" +icon = "🕹️" + +identity = """\ +Battle-hardened dev with expertise in Unity, Unreal, and custom engines. Ten years shipping across mobile, console, and PC. Writes clean, performant code.""" + +communicationStyle = """\ +Speaks like a speedrunner - direct, milestone-focused, always optimizing for the fastest path to ship""" + +principles = """\ +60fps is non-negotiable. Write code designers can iterate without fear. Ship early, ship often, iterate on player feedback. Red-green-refactor: tests first, implementation second.""" + +# ────────────────────────────────────────────────────────────────── +# Menu customization docs +# ────────────────────────────────────────────────────────────────── + +# ────────────────────────────────────────────────────────────────── +# Injected prompts +# ────────────────────────────────────────────────────────────────── +[inject] +before = "" +after = "" diff --git a/src/agents/gds-agent-game-dev/scripts/resolve-customization.py b/src/agents/gds-agent-game-dev/scripts/resolve-customization.py new file mode 100755 index 0000000..b4c6aae --- /dev/null +++ b/src/agents/gds-agent-game-dev/scripts/resolve-customization.py @@ -0,0 +1,182 @@ +#!/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 os +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 Exception 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* looks like a ``[[menu]]`` array of tables with ``code`` keys.""" + return ( + isinstance(value, list) + and len(value) > 0 + and isinstance(value[0], dict) + and "code" in value[0] + ) + + +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} + for item in override: + 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) + 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/agents/gds-agent-game-qa/customize.toml b/src/agents/gds-agent-game-qa/customize.toml new file mode 100644 index 0000000..309deb2 --- /dev/null +++ b/src/agents/gds-agent-game-qa/customize.toml @@ -0,0 +1,56 @@ +# ────────────────────────────────────────────────────────────────── +# Customization Defaults: gds-agent-game-qa +# This file defines all customizable fields for this skill. +# DO NOT EDIT THIS FILE -- it is overwritten on every update. +# +# HOW TO CUSTOMIZE: +# 1. Create an override file with only the fields you want to change: +# _bmad/customizations/gds-agent-game-qa.toml (team/org, committed to git) +# _bmad/customizations/gds-agent-game-qa.user.toml (personal, gitignored) +# 2. Copy just the fields you want to override into your file. +# Unmentioned fields inherit from this defaults file. +# 3. For array fields (like additional_resources), include the +# complete array you want -- arrays replace, not append. +# ────────────────────────────────────────────────────────────────── + +# Additional resource files loaded into agent context on activation. +# Paths are relative to {project-root}. +additional_resources = [] + +# ────────────────────────────────────────────────────────────────── +# Skill metadata - used by the installer for manifest generation. +# ────────────────────────────────────────────────────────────────── +[metadata] +type = "agent" +name = "gds-agent-game-qa" +module = "gds" +role = "Game QA Architect + Test Automation Specialist" +capabilities = "test framework setup, test design, test automation, E2E scaffolding, playtest planning, performance testing, test review" + +# ────────────────────────────────────────────────────────────────── +# Agent persona +# ────────────────────────────────────────────────────────────────── +[persona] +displayName = "GLaDOS" +title = "Game QA Architect" +icon = "🧪" + +identity = """\ +Senior QA architect with 12+ years in game testing across Unity, Unreal, and Godot. Expert in automated testing frameworks, performance profiling, and shipping bug-free games on console, PC, and mobile.""" + +communicationStyle = """\ +Speaks like GLaDOS, the AI from Valve's 'Portal' series. Runs tests because we can. 'Trust, but verify with tests.'""" + +principles = """\ +Test what matters: gameplay feel, performance, progression. Automated tests catch regressions, humans catch fun problems. Every shipped bug is a process failure, not a people failure. Flaky tests are worse than no tests - they erode trust. Profile before optimize, test before ship.""" + +# ────────────────────────────────────────────────────────────────── +# Menu customization docs +# ────────────────────────────────────────────────────────────────── + +# ────────────────────────────────────────────────────────────────── +# Injected prompts +# ────────────────────────────────────────────────────────────────── +[inject] +before = "" +after = "" diff --git a/src/agents/gds-agent-game-qa/scripts/resolve-customization.py b/src/agents/gds-agent-game-qa/scripts/resolve-customization.py new file mode 100755 index 0000000..b4c6aae --- /dev/null +++ b/src/agents/gds-agent-game-qa/scripts/resolve-customization.py @@ -0,0 +1,182 @@ +#!/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 os +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 Exception 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* looks like a ``[[menu]]`` array of tables with ``code`` keys.""" + return ( + isinstance(value, list) + and len(value) > 0 + and isinstance(value[0], dict) + and "code" in value[0] + ) + + +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} + for item in override: + 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) + 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/agents/gds-agent-game-scrum-master/customize.toml b/src/agents/gds-agent-game-scrum-master/customize.toml new file mode 100644 index 0000000..cb5cbaf --- /dev/null +++ b/src/agents/gds-agent-game-scrum-master/customize.toml @@ -0,0 +1,56 @@ +# ────────────────────────────────────────────────────────────────── +# Customization Defaults: gds-agent-game-scrum-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/gds-agent-game-scrum-master.toml (team/org, committed to git) +# _bmad/customizations/gds-agent-game-scrum-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 = "gds-agent-game-scrum-master" +module = "gds" +role = "Game Development Scrum Master + Sprint Orchestrator" +capabilities = "sprint planning, sprint status, story creation, retrospectives, course correction" + +# ────────────────────────────────────────────────────────────────── +# Agent persona +# ────────────────────────────────────────────────────────────────── +[persona] +displayName = "Max" +title = "Game Dev Scrum Master" +icon = "🎯" + +identity = """\ +Certified Scrum Master specializing in game dev workflows. Expert at coordinating multi-disciplinary teams and translating GDDs into actionable stories.""" + +communicationStyle = """\ +Talks in game terminology - milestones are save points, handoffs are level transitions, blockers are boss fights""" + +principles = """\ +Every sprint delivers playable increments. Clean separation between design and implementation. Keep the team moving through each phase. Stories are single source of truth for implementation.""" + +# ────────────────────────────────────────────────────────────────── +# Menu customization docs +# ────────────────────────────────────────────────────────────────── + +# ────────────────────────────────────────────────────────────────── +# Injected prompts +# ────────────────────────────────────────────────────────────────── +[inject] +before = "" +after = "" diff --git a/src/agents/gds-agent-game-scrum-master/scripts/resolve-customization.py b/src/agents/gds-agent-game-scrum-master/scripts/resolve-customization.py new file mode 100755 index 0000000..b4c6aae --- /dev/null +++ b/src/agents/gds-agent-game-scrum-master/scripts/resolve-customization.py @@ -0,0 +1,182 @@ +#!/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 os +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 Exception 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* looks like a ``[[menu]]`` array of tables with ``code`` keys.""" + return ( + isinstance(value, list) + and len(value) > 0 + and isinstance(value[0], dict) + and "code" in value[0] + ) + + +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} + for item in override: + 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) + 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/agents/gds-agent-game-solo-dev/customize.toml b/src/agents/gds-agent-game-solo-dev/customize.toml new file mode 100644 index 0000000..7b8b3e1 --- /dev/null +++ b/src/agents/gds-agent-game-solo-dev/customize.toml @@ -0,0 +1,56 @@ +# ────────────────────────────────────────────────────────────────── +# Customization Defaults: gds-agent-game-solo-dev +# This file defines all customizable fields for this skill. +# DO NOT EDIT THIS FILE -- it is overwritten on every update. +# +# HOW TO CUSTOMIZE: +# 1. Create an override file with only the fields you want to change: +# _bmad/customizations/gds-agent-game-solo-dev.toml (team/org, committed to git) +# _bmad/customizations/gds-agent-game-solo-dev.user.toml (personal, gitignored) +# 2. Copy just the fields you want to override into your file. +# Unmentioned fields inherit from this defaults file. +# 3. For array fields (like additional_resources), include the +# complete array you want -- arrays replace, not append. +# ────────────────────────────────────────────────────────────────── + +# Additional resource files loaded into agent context on activation. +# Paths are relative to {project-root}. +additional_resources = [] + +# ────────────────────────────────────────────────────────────────── +# Skill metadata - used by the installer for manifest generation. +# ────────────────────────────────────────────────────────────────── +[metadata] +type = "agent" +name = "gds-agent-game-solo-dev" +module = "gds" +role = "Elite Indie Game Developer + Quick Flow Specialist" +capabilities = "rapid prototyping, solo development, tech specs, code review, test framework, quick flow" + +# ────────────────────────────────────────────────────────────────── +# Agent persona +# ────────────────────────────────────────────────────────────────── +[persona] +displayName = "Indie" +title = "Game Solo Dev" +icon = "🎮" + +identity = """\ +Indie is a battle-hardened solo game developer who ships complete games from concept to launch. Expert in Unity, Unreal, and Godot, they've shipped titles across mobile, PC, and console. Lives and breathes the Quick Flow workflow - prototyping fast, iterating faster, and shipping before the hype dies. No team politics, no endless meetings - just pure, focused game development.""" + +communicationStyle = """\ +Direct, confident, and gameplay-focused. Uses dev slang, thinks in game feel and player experience. Every response moves the game closer to ship. 'Does it feel good? Ship it.'""" + +principles = """\ +Prototype fast, fail fast, iterate faster. Quick Flow is the indie way. A playable build beats a perfect design doc. Ship early, playtest often. 60fps is non-negotiable. Performance is a feature. The core loop must be fun before anything else matters.""" + +# ────────────────────────────────────────────────────────────────── +# Menu customization docs +# ────────────────────────────────────────────────────────────────── + +# ────────────────────────────────────────────────────────────────── +# Injected prompts +# ────────────────────────────────────────────────────────────────── +[inject] +before = "" +after = "" diff --git a/src/agents/gds-agent-game-solo-dev/scripts/resolve-customization.py b/src/agents/gds-agent-game-solo-dev/scripts/resolve-customization.py new file mode 100755 index 0000000..b4c6aae --- /dev/null +++ b/src/agents/gds-agent-game-solo-dev/scripts/resolve-customization.py @@ -0,0 +1,182 @@ +#!/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 os +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 Exception 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* looks like a ``[[menu]]`` array of tables with ``code`` keys.""" + return ( + isinstance(value, list) + and len(value) > 0 + and isinstance(value[0], dict) + and "code" in value[0] + ) + + +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} + for item in override: + 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) + 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/agents/gds-agent-tech-writer/customize.toml b/src/agents/gds-agent-tech-writer/customize.toml new file mode 100644 index 0000000..cc73104 --- /dev/null +++ b/src/agents/gds-agent-tech-writer/customize.toml @@ -0,0 +1,56 @@ +# ────────────────────────────────────────────────────────────────── +# Customization Defaults: gds-agent-tech-writer +# This file defines all customizable fields for this skill. +# DO NOT EDIT THIS FILE -- it is overwritten on every update. +# +# HOW TO CUSTOMIZE: +# 1. Create an override file with only the fields you want to change: +# _bmad/customizations/gds-agent-tech-writer.toml (team/org, committed to git) +# _bmad/customizations/gds-agent-tech-writer.user.toml (personal, gitignored) +# 2. Copy just the fields you want to override into your file. +# Unmentioned fields inherit from this defaults file. +# 3. For array fields (like additional_resources), include the +# complete array you want -- arrays replace, not append. +# ────────────────────────────────────────────────────────────────── + +# Additional resource files loaded into agent context on activation. +# Paths are relative to {project-root}. +additional_resources = [] + +# ────────────────────────────────────────────────────────────────── +# Skill metadata - used by the installer for manifest generation. +# ────────────────────────────────────────────────────────────────── +[metadata] +type = "agent" +name = "gds-agent-tech-writer" +module = "gds" +role = "Technical Documentation Specialist + Knowledge Curator" +capabilities = "documentation, Mermaid diagrams, standards compliance, concept explanation" + +# ────────────────────────────────────────────────────────────────── +# Agent persona +# ────────────────────────────────────────────────────────────────── +[persona] +displayName = "Paige" +title = "Technical Writer" +icon = "📚" + +identity = """\ +Experienced technical writer expert in CommonMark, DITA, OpenAPI. Master of clarity - transforms complex concepts into accessible structured documentation.""" + +communicationStyle = """\ +Patient educator who explains like teaching a friend. Uses analogies that make complex simple, celebrates clarity when it shines.""" + +principles = """\ +Every Technical Document I touch helps someone accomplish a task. Clarity above all, and every word and phrase serves a purpose without being overly wordy. A picture or diagram is worth thousands of words - include diagrams over drawn out text. Understand the intended audience or clarify with the user to know when to simplify vs when to be detailed.""" + +# ────────────────────────────────────────────────────────────────── +# Menu customization docs +# ────────────────────────────────────────────────────────────────── + +# ────────────────────────────────────────────────────────────────── +# Injected prompts +# ────────────────────────────────────────────────────────────────── +[inject] +before = "" +after = "" diff --git a/src/agents/gds-agent-tech-writer/scripts/resolve-customization.py b/src/agents/gds-agent-tech-writer/scripts/resolve-customization.py new file mode 100755 index 0000000..b4c6aae --- /dev/null +++ b/src/agents/gds-agent-tech-writer/scripts/resolve-customization.py @@ -0,0 +1,182 @@ +#!/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 os +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 Exception 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* looks like a ``[[menu]]`` array of tables with ``code`` keys.""" + return ( + isinstance(value, list) + and len(value) > 0 + and isinstance(value[0], dict) + and "code" in value[0] + ) + + +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} + for item in override: + 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) + 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/workflows/1-preproduction/gds-brainstorm-game/SKILL.md b/src/workflows/1-preproduction/gds-brainstorm-game/SKILL.md index fcc91b5..1e36668 100644 --- a/src/workflows/1-preproduction/gds-brainstorm-game/SKILL.md +++ b/src/workflows/1-preproduction/gds-brainstorm-game/SKILL.md @@ -3,4 +3,20 @@ name: gds-brainstorm-game description: 'Facilitate game brainstorming sessions with game-specific techniques. Use when the user says "brainstorm game" or "game ideas"' --- +## Resolve Customization + +Resolve `inject` and `additional_resources` from customization: +Run: `python ./scripts/resolve-customization.py gds-brainstorm-game --key inject --key additional_resources` +Use the JSON output as resolved values. + +If `inject.before` is not empty, incorporate its content as high-priority context. +If `additional_resources` is not empty, read each listed file and incorporate as reference context. + Follow the instructions in ./workflow.md. + +## Post-Workflow Customization + +After the workflow completes, resolve `inject.after` from customization: +Run: `python ./scripts/resolve-customization.py gds-brainstorm-game --key inject.after` + +If resolved `inject.after` is not empty, incorporate its content as a final checklist or validation gate. diff --git a/src/workflows/1-preproduction/gds-brainstorm-game/customize.toml b/src/workflows/1-preproduction/gds-brainstorm-game/customize.toml new file mode 100644 index 0000000..ac08976 --- /dev/null +++ b/src/workflows/1-preproduction/gds-brainstorm-game/customize.toml @@ -0,0 +1,27 @@ +# ────────────────────────────────────────────────────────────────── +# Customization Defaults: gds-brainstorm-game +# This file defines all customizable fields for this skill. +# DO NOT EDIT THIS FILE -- it is overwritten on every update. +# +# HOW TO CUSTOMIZE: +# 1. Create an override file with only the fields you want to change: +# _bmad/customizations/gds-brainstorm-game.toml (team/org, committed to git) +# _bmad/customizations/gds-brainstorm-game.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/workflows/1-preproduction/gds-brainstorm-game/scripts/resolve-customization.py b/src/workflows/1-preproduction/gds-brainstorm-game/scripts/resolve-customization.py new file mode 100755 index 0000000..b4c6aae --- /dev/null +++ b/src/workflows/1-preproduction/gds-brainstorm-game/scripts/resolve-customization.py @@ -0,0 +1,182 @@ +#!/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 os +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 Exception 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* looks like a ``[[menu]]`` array of tables with ``code`` keys.""" + return ( + isinstance(value, list) + and len(value) > 0 + and isinstance(value[0], dict) + and "code" in value[0] + ) + + +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} + for item in override: + 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) + 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/workflows/1-preproduction/gds-create-game-brief/SKILL.md b/src/workflows/1-preproduction/gds-create-game-brief/SKILL.md index e77c821..6aafc07 100644 --- a/src/workflows/1-preproduction/gds-create-game-brief/SKILL.md +++ b/src/workflows/1-preproduction/gds-create-game-brief/SKILL.md @@ -3,4 +3,20 @@ name: gds-create-game-brief description: 'Interactive game brief creation guiding users through defining their game vision. Use when the user says "game brief" or "create brief"' --- +## Resolve Customization + +Resolve `inject` and `additional_resources` from customization: +Run: `python ./scripts/resolve-customization.py gds-create-game-brief --key inject --key additional_resources` +Use the JSON output as resolved values. + +If `inject.before` is not empty, incorporate its content as high-priority context. +If `additional_resources` is not empty, read each listed file and incorporate as reference context. + Follow the instructions in ./workflow.md. + +## Post-Workflow Customization + +After the workflow completes, resolve `inject.after` from customization: +Run: `python ./scripts/resolve-customization.py gds-create-game-brief --key inject.after` + +If resolved `inject.after` is not empty, incorporate its content as a final checklist or validation gate. diff --git a/src/workflows/1-preproduction/gds-create-game-brief/customize.toml b/src/workflows/1-preproduction/gds-create-game-brief/customize.toml new file mode 100644 index 0000000..be46811 --- /dev/null +++ b/src/workflows/1-preproduction/gds-create-game-brief/customize.toml @@ -0,0 +1,27 @@ +# ────────────────────────────────────────────────────────────────── +# Customization Defaults: gds-create-game-brief +# This file defines all customizable fields for this skill. +# DO NOT EDIT THIS FILE -- it is overwritten on every update. +# +# HOW TO CUSTOMIZE: +# 1. Create an override file with only the fields you want to change: +# _bmad/customizations/gds-create-game-brief.toml (team/org, committed to git) +# _bmad/customizations/gds-create-game-brief.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/workflows/1-preproduction/gds-create-game-brief/scripts/resolve-customization.py b/src/workflows/1-preproduction/gds-create-game-brief/scripts/resolve-customization.py new file mode 100755 index 0000000..b4c6aae --- /dev/null +++ b/src/workflows/1-preproduction/gds-create-game-brief/scripts/resolve-customization.py @@ -0,0 +1,182 @@ +#!/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 os +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 Exception 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* looks like a ``[[menu]]`` array of tables with ``code`` keys.""" + return ( + isinstance(value, list) + and len(value) > 0 + and isinstance(value[0], dict) + and "code" in value[0] + ) + + +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} + for item in override: + 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) + 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/workflows/1-preproduction/research/gds-domain-research/SKILL.md b/src/workflows/1-preproduction/research/gds-domain-research/SKILL.md index 0c44947..70c2fb3 100644 --- a/src/workflows/1-preproduction/research/gds-domain-research/SKILL.md +++ b/src/workflows/1-preproduction/research/gds-domain-research/SKILL.md @@ -3,4 +3,20 @@ name: gds-domain-research description: 'Conduct game domain and industry research. Use when the user says "lets create a research report on [game domain or industry]"' --- +## Resolve Customization + +Resolve `inject` and `additional_resources` from customization: +Run: `python ./scripts/resolve-customization.py gds-domain-research --key inject --key additional_resources` +Use the JSON output as resolved values. + +If `inject.before` is not empty, incorporate its content as high-priority context. +If `additional_resources` is not empty, read each listed file and incorporate as reference context. + Follow the instructions in ./workflow.md. + +## Post-Workflow Customization + +After the workflow completes, resolve `inject.after` from customization: +Run: `python ./scripts/resolve-customization.py gds-domain-research --key inject.after` + +If resolved `inject.after` is not empty, incorporate its content as a final checklist or validation gate. diff --git a/src/workflows/1-preproduction/research/gds-domain-research/customize.toml b/src/workflows/1-preproduction/research/gds-domain-research/customize.toml new file mode 100644 index 0000000..3d8df48 --- /dev/null +++ b/src/workflows/1-preproduction/research/gds-domain-research/customize.toml @@ -0,0 +1,27 @@ +# ────────────────────────────────────────────────────────────────── +# Customization Defaults: gds-domain-research +# This file defines all customizable fields for this skill. +# DO NOT EDIT THIS FILE -- it is overwritten on every update. +# +# HOW TO CUSTOMIZE: +# 1. Create an override file with only the fields you want to change: +# _bmad/customizations/gds-domain-research.toml (team/org, committed to git) +# _bmad/customizations/gds-domain-research.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/workflows/1-preproduction/research/gds-domain-research/scripts/resolve-customization.py b/src/workflows/1-preproduction/research/gds-domain-research/scripts/resolve-customization.py new file mode 100755 index 0000000..b4c6aae --- /dev/null +++ b/src/workflows/1-preproduction/research/gds-domain-research/scripts/resolve-customization.py @@ -0,0 +1,182 @@ +#!/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 os +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 Exception 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* looks like a ``[[menu]]`` array of tables with ``code`` keys.""" + return ( + isinstance(value, list) + and len(value) > 0 + and isinstance(value[0], dict) + and "code" in value[0] + ) + + +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} + for item in override: + 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) + 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/workflows/2-design/gds-create-gdd/SKILL.md b/src/workflows/2-design/gds-create-gdd/SKILL.md index a5fa0f5..78f4862 100644 --- a/src/workflows/2-design/gds-create-gdd/SKILL.md +++ b/src/workflows/2-design/gds-create-gdd/SKILL.md @@ -3,4 +3,20 @@ name: gds-create-gdd description: 'Create Game Design Documents with mechanics and implementation guidance. Use when the user says "create GDD" or "game design document"' --- +## Resolve Customization + +Resolve `inject` and `additional_resources` from customization: +Run: `python ./scripts/resolve-customization.py gds-create-gdd --key inject --key additional_resources` +Use the JSON output as resolved values. + +If `inject.before` is not empty, incorporate its content as high-priority context. +If `additional_resources` is not empty, read each listed file and incorporate as reference context. + Follow the instructions in ./workflow.md. + +## Post-Workflow Customization + +After the workflow completes, resolve `inject.after` from customization: +Run: `python ./scripts/resolve-customization.py gds-create-gdd --key inject.after` + +If resolved `inject.after` is not empty, incorporate its content as a final checklist or validation gate. diff --git a/src/workflows/2-design/gds-create-gdd/customize.toml b/src/workflows/2-design/gds-create-gdd/customize.toml new file mode 100644 index 0000000..a8f2843 --- /dev/null +++ b/src/workflows/2-design/gds-create-gdd/customize.toml @@ -0,0 +1,27 @@ +# ────────────────────────────────────────────────────────────────── +# Customization Defaults: gds-create-gdd +# This file defines all customizable fields for this skill. +# DO NOT EDIT THIS FILE -- it is overwritten on every update. +# +# HOW TO CUSTOMIZE: +# 1. Create an override file with only the fields you want to change: +# _bmad/customizations/gds-create-gdd.toml (team/org, committed to git) +# _bmad/customizations/gds-create-gdd.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/workflows/2-design/gds-create-gdd/scripts/resolve-customization.py b/src/workflows/2-design/gds-create-gdd/scripts/resolve-customization.py new file mode 100755 index 0000000..b4c6aae --- /dev/null +++ b/src/workflows/2-design/gds-create-gdd/scripts/resolve-customization.py @@ -0,0 +1,182 @@ +#!/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 os +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 Exception 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* looks like a ``[[menu]]`` array of tables with ``code`` keys.""" + return ( + isinstance(value, list) + and len(value) > 0 + and isinstance(value[0], dict) + and "code" in value[0] + ) + + +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} + for item in override: + 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) + 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/workflows/2-design/gds-create-narrative/SKILL.md b/src/workflows/2-design/gds-create-narrative/SKILL.md index 914caff..ba18192 100644 --- a/src/workflows/2-design/gds-create-narrative/SKILL.md +++ b/src/workflows/2-design/gds-create-narrative/SKILL.md @@ -3,4 +3,20 @@ name: gds-create-narrative description: 'Create comprehensive narrative documentation with story structure and world-building. Use when the user says "narrative design" or "create narrative"' --- +## Resolve Customization + +Resolve `inject` and `additional_resources` from customization: +Run: `python ./scripts/resolve-customization.py gds-create-narrative --key inject --key additional_resources` +Use the JSON output as resolved values. + +If `inject.before` is not empty, incorporate its content as high-priority context. +If `additional_resources` is not empty, read each listed file and incorporate as reference context. + Follow the instructions in ./workflow.md. + +## Post-Workflow Customization + +After the workflow completes, resolve `inject.after` from customization: +Run: `python ./scripts/resolve-customization.py gds-create-narrative --key inject.after` + +If resolved `inject.after` is not empty, incorporate its content as a final checklist or validation gate. diff --git a/src/workflows/2-design/gds-create-narrative/customize.toml b/src/workflows/2-design/gds-create-narrative/customize.toml new file mode 100644 index 0000000..2676632 --- /dev/null +++ b/src/workflows/2-design/gds-create-narrative/customize.toml @@ -0,0 +1,27 @@ +# ────────────────────────────────────────────────────────────────── +# Customization Defaults: gds-create-narrative +# This file defines all customizable fields for this skill. +# DO NOT EDIT THIS FILE -- it is overwritten on every update. +# +# HOW TO CUSTOMIZE: +# 1. Create an override file with only the fields you want to change: +# _bmad/customizations/gds-create-narrative.toml (team/org, committed to git) +# _bmad/customizations/gds-create-narrative.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/workflows/2-design/gds-create-narrative/scripts/resolve-customization.py b/src/workflows/2-design/gds-create-narrative/scripts/resolve-customization.py new file mode 100755 index 0000000..b4c6aae --- /dev/null +++ b/src/workflows/2-design/gds-create-narrative/scripts/resolve-customization.py @@ -0,0 +1,182 @@ +#!/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 os +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 Exception 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* looks like a ``[[menu]]`` array of tables with ``code`` keys.""" + return ( + isinstance(value, list) + and len(value) > 0 + and isinstance(value[0], dict) + and "code" in value[0] + ) + + +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} + for item in override: + 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) + 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/workflows/2-design/gds-create-ux-design/SKILL.md b/src/workflows/2-design/gds-create-ux-design/SKILL.md index f902f31..1287c5d 100644 --- a/src/workflows/2-design/gds-create-ux-design/SKILL.md +++ b/src/workflows/2-design/gds-create-ux-design/SKILL.md @@ -3,4 +3,20 @@ name: gds-create-ux-design description: 'Plan UX patterns and design specifications for game UI/HUD elements. Use when the user says "lets create UX design" or "create UX specifications" or "help me plan the game UX"' --- +## Resolve Customization + +Resolve `inject` and `additional_resources` from customization: +Run: `python ./scripts/resolve-customization.py gds-create-ux-design --key inject --key additional_resources` +Use the JSON output as resolved values. + +If `inject.before` is not empty, incorporate its content as high-priority context. +If `additional_resources` is not empty, read each listed file and incorporate as reference context. + Follow the instructions in ./workflow.md. + +## Post-Workflow Customization + +After the workflow completes, resolve `inject.after` from customization: +Run: `python ./scripts/resolve-customization.py gds-create-ux-design --key inject.after` + +If resolved `inject.after` is not empty, incorporate its content as a final checklist or validation gate. diff --git a/src/workflows/2-design/gds-create-ux-design/customize.toml b/src/workflows/2-design/gds-create-ux-design/customize.toml new file mode 100644 index 0000000..9c4bd9a --- /dev/null +++ b/src/workflows/2-design/gds-create-ux-design/customize.toml @@ -0,0 +1,27 @@ +# ────────────────────────────────────────────────────────────────── +# Customization Defaults: gds-create-ux-design +# This file defines all customizable fields for this skill. +# DO NOT EDIT THIS FILE -- it is overwritten on every update. +# +# HOW TO CUSTOMIZE: +# 1. Create an override file with only the fields you want to change: +# _bmad/customizations/gds-create-ux-design.toml (team/org, committed to git) +# _bmad/customizations/gds-create-ux-design.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/workflows/2-design/gds-create-ux-design/scripts/resolve-customization.py b/src/workflows/2-design/gds-create-ux-design/scripts/resolve-customization.py new file mode 100755 index 0000000..b4c6aae --- /dev/null +++ b/src/workflows/2-design/gds-create-ux-design/scripts/resolve-customization.py @@ -0,0 +1,182 @@ +#!/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 os +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 Exception 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* looks like a ``[[menu]]`` array of tables with ``code`` keys.""" + return ( + isinstance(value, list) + and len(value) > 0 + and isinstance(value[0], dict) + and "code" in value[0] + ) + + +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} + for item in override: + 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) + 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/workflows/3-technical/gds-check-implementation-readiness/SKILL.md b/src/workflows/3-technical/gds-check-implementation-readiness/SKILL.md index 6c6b97f..e5f0321 100644 --- a/src/workflows/3-technical/gds-check-implementation-readiness/SKILL.md +++ b/src/workflows/3-technical/gds-check-implementation-readiness/SKILL.md @@ -3,4 +3,20 @@ name: gds-check-implementation-readiness description: 'Verify GDD, UX, Architecture, and Epics alignment before production. Use when the user says "check readiness" or "implementation readiness"' --- +## Resolve Customization + +Resolve `inject` and `additional_resources` from customization: +Run: `python ./scripts/resolve-customization.py gds-check-implementation-readiness --key inject --key additional_resources` +Use the JSON output as resolved values. + +If `inject.before` is not empty, incorporate its content as high-priority context. +If `additional_resources` is not empty, read each listed file and incorporate as reference context. + Follow the instructions in ./workflow.md. + +## Post-Workflow Customization + +After the workflow completes, resolve `inject.after` from customization: +Run: `python ./scripts/resolve-customization.py gds-check-implementation-readiness --key inject.after` + +If resolved `inject.after` is not empty, incorporate its content as a final checklist or validation gate. diff --git a/src/workflows/3-technical/gds-check-implementation-readiness/customize.toml b/src/workflows/3-technical/gds-check-implementation-readiness/customize.toml new file mode 100644 index 0000000..93616b4 --- /dev/null +++ b/src/workflows/3-technical/gds-check-implementation-readiness/customize.toml @@ -0,0 +1,27 @@ +# ────────────────────────────────────────────────────────────────── +# Customization Defaults: gds-check-implementation-readiness +# This file defines all customizable fields for this skill. +# DO NOT EDIT THIS FILE -- it is overwritten on every update. +# +# HOW TO CUSTOMIZE: +# 1. Create an override file with only the fields you want to change: +# _bmad/customizations/gds-check-implementation-readiness.toml (team/org, committed to git) +# _bmad/customizations/gds-check-implementation-readiness.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/workflows/3-technical/gds-check-implementation-readiness/scripts/resolve-customization.py b/src/workflows/3-technical/gds-check-implementation-readiness/scripts/resolve-customization.py new file mode 100755 index 0000000..b4c6aae --- /dev/null +++ b/src/workflows/3-technical/gds-check-implementation-readiness/scripts/resolve-customization.py @@ -0,0 +1,182 @@ +#!/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 os +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 Exception 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* looks like a ``[[menu]]`` array of tables with ``code`` keys.""" + return ( + isinstance(value, list) + and len(value) > 0 + and isinstance(value[0], dict) + and "code" in value[0] + ) + + +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} + for item in override: + 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) + 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/workflows/3-technical/gds-create-epics-and-stories/SKILL.md b/src/workflows/3-technical/gds-create-epics-and-stories/SKILL.md index f3b0fe3..7a16e79 100644 --- a/src/workflows/3-technical/gds-create-epics-and-stories/SKILL.md +++ b/src/workflows/3-technical/gds-create-epics-and-stories/SKILL.md @@ -3,4 +3,20 @@ name: gds-create-epics-and-stories description: 'Create Epics and Stories from GDD requirements for development. Use when the user says "create epics" or "create stories"' --- +## Resolve Customization + +Resolve `inject` and `additional_resources` from customization: +Run: `python ./scripts/resolve-customization.py gds-create-epics-and-stories --key inject --key additional_resources` +Use the JSON output as resolved values. + +If `inject.before` is not empty, incorporate its content as high-priority context. +If `additional_resources` is not empty, read each listed file and incorporate as reference context. + Follow the instructions in ./workflow.md. + +## Post-Workflow Customization + +After the workflow completes, resolve `inject.after` from customization: +Run: `python ./scripts/resolve-customization.py gds-create-epics-and-stories --key inject.after` + +If resolved `inject.after` is not empty, incorporate its content as a final checklist or validation gate. diff --git a/src/workflows/3-technical/gds-create-epics-and-stories/customize.toml b/src/workflows/3-technical/gds-create-epics-and-stories/customize.toml new file mode 100644 index 0000000..42e93d3 --- /dev/null +++ b/src/workflows/3-technical/gds-create-epics-and-stories/customize.toml @@ -0,0 +1,27 @@ +# ────────────────────────────────────────────────────────────────── +# Customization Defaults: gds-create-epics-and-stories +# This file defines all customizable fields for this skill. +# DO NOT EDIT THIS FILE -- it is overwritten on every update. +# +# HOW TO CUSTOMIZE: +# 1. Create an override file with only the fields you want to change: +# _bmad/customizations/gds-create-epics-and-stories.toml (team/org, committed to git) +# _bmad/customizations/gds-create-epics-and-stories.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/workflows/3-technical/gds-create-epics-and-stories/scripts/resolve-customization.py b/src/workflows/3-technical/gds-create-epics-and-stories/scripts/resolve-customization.py new file mode 100755 index 0000000..b4c6aae --- /dev/null +++ b/src/workflows/3-technical/gds-create-epics-and-stories/scripts/resolve-customization.py @@ -0,0 +1,182 @@ +#!/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 os +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 Exception 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* looks like a ``[[menu]]`` array of tables with ``code`` keys.""" + return ( + isinstance(value, list) + and len(value) > 0 + and isinstance(value[0], dict) + and "code" in value[0] + ) + + +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} + for item in override: + 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) + 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/workflows/3-technical/gds-game-architecture/SKILL.md b/src/workflows/3-technical/gds-game-architecture/SKILL.md index b3cbf31..057d307 100644 --- a/src/workflows/3-technical/gds-game-architecture/SKILL.md +++ b/src/workflows/3-technical/gds-game-architecture/SKILL.md @@ -3,4 +3,20 @@ name: gds-game-architecture description: 'Design scale-adaptive game architecture with engine systems and networking. Use when the user says "game architecture" or "design architecture"' --- +## Resolve Customization + +Resolve `inject` and `additional_resources` from customization: +Run: `python ./scripts/resolve-customization.py gds-game-architecture --key inject --key additional_resources` +Use the JSON output as resolved values. + +If `inject.before` is not empty, incorporate its content as high-priority context. +If `additional_resources` is not empty, read each listed file and incorporate as reference context. + Follow the instructions in ./workflow.md. + +## Post-Workflow Customization + +After the workflow completes, resolve `inject.after` from customization: +Run: `python ./scripts/resolve-customization.py gds-game-architecture --key inject.after` + +If resolved `inject.after` is not empty, incorporate its content as a final checklist or validation gate. diff --git a/src/workflows/3-technical/gds-game-architecture/customize.toml b/src/workflows/3-technical/gds-game-architecture/customize.toml new file mode 100644 index 0000000..d9bddd7 --- /dev/null +++ b/src/workflows/3-technical/gds-game-architecture/customize.toml @@ -0,0 +1,27 @@ +# ────────────────────────────────────────────────────────────────── +# Customization Defaults: gds-game-architecture +# This file defines all customizable fields for this skill. +# DO NOT EDIT THIS FILE -- it is overwritten on every update. +# +# HOW TO CUSTOMIZE: +# 1. Create an override file with only the fields you want to change: +# _bmad/customizations/gds-game-architecture.toml (team/org, committed to git) +# _bmad/customizations/gds-game-architecture.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/workflows/3-technical/gds-game-architecture/scripts/resolve-customization.py b/src/workflows/3-technical/gds-game-architecture/scripts/resolve-customization.py new file mode 100755 index 0000000..b4c6aae --- /dev/null +++ b/src/workflows/3-technical/gds-game-architecture/scripts/resolve-customization.py @@ -0,0 +1,182 @@ +#!/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 os +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 Exception 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* looks like a ``[[menu]]`` array of tables with ``code`` keys.""" + return ( + isinstance(value, list) + and len(value) > 0 + and isinstance(value[0], dict) + and "code" in value[0] + ) + + +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} + for item in override: + 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) + 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/workflows/3-technical/gds-generate-project-context/SKILL.md b/src/workflows/3-technical/gds-generate-project-context/SKILL.md index 9599423..2c70c59 100644 --- a/src/workflows/3-technical/gds-generate-project-context/SKILL.md +++ b/src/workflows/3-technical/gds-generate-project-context/SKILL.md @@ -3,4 +3,20 @@ name: gds-generate-project-context description: 'Create optimized project-context.md for AI agent consistency. Use when the user says "project context" or "generate context"' --- +## Resolve Customization + +Resolve `inject` and `additional_resources` from customization: +Run: `python ./scripts/resolve-customization.py gds-generate-project-context --key inject --key additional_resources` +Use the JSON output as resolved values. + +If `inject.before` is not empty, incorporate its content as high-priority context. +If `additional_resources` is not empty, read each listed file and incorporate as reference context. + Follow the instructions in ./workflow.md. + +## Post-Workflow Customization + +After the workflow completes, resolve `inject.after` from customization: +Run: `python ./scripts/resolve-customization.py gds-generate-project-context --key inject.after` + +If resolved `inject.after` is not empty, incorporate its content as a final checklist or validation gate. diff --git a/src/workflows/3-technical/gds-generate-project-context/customize.toml b/src/workflows/3-technical/gds-generate-project-context/customize.toml new file mode 100644 index 0000000..bbed8b8 --- /dev/null +++ b/src/workflows/3-technical/gds-generate-project-context/customize.toml @@ -0,0 +1,27 @@ +# ────────────────────────────────────────────────────────────────── +# Customization Defaults: gds-generate-project-context +# This file defines all customizable fields for this skill. +# DO NOT EDIT THIS FILE -- it is overwritten on every update. +# +# HOW TO CUSTOMIZE: +# 1. Create an override file with only the fields you want to change: +# _bmad/customizations/gds-generate-project-context.toml (team/org, committed to git) +# _bmad/customizations/gds-generate-project-context.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/workflows/3-technical/gds-generate-project-context/scripts/resolve-customization.py b/src/workflows/3-technical/gds-generate-project-context/scripts/resolve-customization.py new file mode 100755 index 0000000..b4c6aae --- /dev/null +++ b/src/workflows/3-technical/gds-generate-project-context/scripts/resolve-customization.py @@ -0,0 +1,182 @@ +#!/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 os +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 Exception 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* looks like a ``[[menu]]`` array of tables with ``code`` keys.""" + return ( + isinstance(value, list) + and len(value) > 0 + and isinstance(value[0], dict) + and "code" in value[0] + ) + + +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} + for item in override: + 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) + 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/workflows/4-production/gds-code-review/SKILL.md b/src/workflows/4-production/gds-code-review/SKILL.md index 896cc25..be0d8f9 100644 --- a/src/workflows/4-production/gds-code-review/SKILL.md +++ b/src/workflows/4-production/gds-code-review/SKILL.md @@ -3,4 +3,20 @@ name: gds-code-review description: 'Perform thorough code review to find bugs and quality issues. Use when the user says "review this code" or "do a code review"' --- +## Resolve Customization + +Resolve `inject` and `additional_resources` from customization: +Run: `python ./scripts/resolve-customization.py gds-code-review --key inject --key additional_resources` +Use the JSON output as resolved values. + +If `inject.before` is not empty, incorporate its content as high-priority context. +If `additional_resources` is not empty, read each listed file and incorporate as reference context. + Follow the instructions in ./workflow.md. + +## Post-Workflow Customization + +After the workflow completes, resolve `inject.after` from customization: +Run: `python ./scripts/resolve-customization.py gds-code-review --key inject.after` + +If resolved `inject.after` is not empty, incorporate its content as a final checklist or validation gate. diff --git a/src/workflows/4-production/gds-code-review/customize.toml b/src/workflows/4-production/gds-code-review/customize.toml new file mode 100644 index 0000000..1e43371 --- /dev/null +++ b/src/workflows/4-production/gds-code-review/customize.toml @@ -0,0 +1,27 @@ +# ────────────────────────────────────────────────────────────────── +# Customization Defaults: gds-code-review +# This file defines all customizable fields for this skill. +# DO NOT EDIT THIS FILE -- it is overwritten on every update. +# +# HOW TO CUSTOMIZE: +# 1. Create an override file with only the fields you want to change: +# _bmad/customizations/gds-code-review.toml (team/org, committed to git) +# _bmad/customizations/gds-code-review.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/workflows/4-production/gds-code-review/scripts/resolve-customization.py b/src/workflows/4-production/gds-code-review/scripts/resolve-customization.py new file mode 100755 index 0000000..b4c6aae --- /dev/null +++ b/src/workflows/4-production/gds-code-review/scripts/resolve-customization.py @@ -0,0 +1,182 @@ +#!/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 os +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 Exception 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* looks like a ``[[menu]]`` array of tables with ``code`` keys.""" + return ( + isinstance(value, list) + and len(value) > 0 + and isinstance(value[0], dict) + and "code" in value[0] + ) + + +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} + for item in override: + 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) + 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/workflows/4-production/gds-correct-course/SKILL.md b/src/workflows/4-production/gds-correct-course/SKILL.md index 9b60b9d..3193096 100644 --- a/src/workflows/4-production/gds-correct-course/SKILL.md +++ b/src/workflows/4-production/gds-correct-course/SKILL.md @@ -3,4 +3,20 @@ name: gds-correct-course description: 'Manage significant changes when sprint implementation is off track. Use when the user says "correct course" or "we need a course correction"' --- +## Resolve Customization + +Resolve `inject` and `additional_resources` from customization: +Run: `python ./scripts/resolve-customization.py gds-correct-course --key inject --key additional_resources` +Use the JSON output as resolved values. + +If `inject.before` is not empty, incorporate its content as high-priority context. +If `additional_resources` is not empty, read each listed file and incorporate as reference context. + Follow the instructions in ./workflow.md. + +## Post-Workflow Customization + +After the workflow completes, resolve `inject.after` from customization: +Run: `python ./scripts/resolve-customization.py gds-correct-course --key inject.after` + +If resolved `inject.after` is not empty, incorporate its content as a final checklist or validation gate. diff --git a/src/workflows/4-production/gds-correct-course/customize.toml b/src/workflows/4-production/gds-correct-course/customize.toml new file mode 100644 index 0000000..3362d74 --- /dev/null +++ b/src/workflows/4-production/gds-correct-course/customize.toml @@ -0,0 +1,27 @@ +# ────────────────────────────────────────────────────────────────── +# Customization Defaults: gds-correct-course +# This file defines all customizable fields for this skill. +# DO NOT EDIT THIS FILE -- it is overwritten on every update. +# +# HOW TO CUSTOMIZE: +# 1. Create an override file with only the fields you want to change: +# _bmad/customizations/gds-correct-course.toml (team/org, committed to git) +# _bmad/customizations/gds-correct-course.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/workflows/4-production/gds-correct-course/scripts/resolve-customization.py b/src/workflows/4-production/gds-correct-course/scripts/resolve-customization.py new file mode 100755 index 0000000..b4c6aae --- /dev/null +++ b/src/workflows/4-production/gds-correct-course/scripts/resolve-customization.py @@ -0,0 +1,182 @@ +#!/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 os +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 Exception 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* looks like a ``[[menu]]`` array of tables with ``code`` keys.""" + return ( + isinstance(value, list) + and len(value) > 0 + and isinstance(value[0], dict) + and "code" in value[0] + ) + + +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} + for item in override: + 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) + 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/workflows/4-production/gds-create-story/SKILL.md b/src/workflows/4-production/gds-create-story/SKILL.md index c0790db..0146c22 100644 --- a/src/workflows/4-production/gds-create-story/SKILL.md +++ b/src/workflows/4-production/gds-create-story/SKILL.md @@ -3,4 +3,20 @@ name: gds-create-story description: 'Creates a dedicated story file with all the context the agent will need to implement it later. Use when the user says "create the next story" or "create story [story identifier]"' --- +## Resolve Customization + +Resolve `inject` and `additional_resources` from customization: +Run: `python ./scripts/resolve-customization.py gds-create-story --key inject --key additional_resources` +Use the JSON output as resolved values. + +If `inject.before` is not empty, incorporate its content as high-priority context. +If `additional_resources` is not empty, read each listed file and incorporate as reference context. + Follow the instructions in ./workflow.md. + +## Post-Workflow Customization + +After the workflow completes, resolve `inject.after` from customization: +Run: `python ./scripts/resolve-customization.py gds-create-story --key inject.after` + +If resolved `inject.after` is not empty, incorporate its content as a final checklist or validation gate. diff --git a/src/workflows/4-production/gds-create-story/customize.toml b/src/workflows/4-production/gds-create-story/customize.toml new file mode 100644 index 0000000..1e93e92 --- /dev/null +++ b/src/workflows/4-production/gds-create-story/customize.toml @@ -0,0 +1,27 @@ +# ────────────────────────────────────────────────────────────────── +# Customization Defaults: gds-create-story +# This file defines all customizable fields for this skill. +# DO NOT EDIT THIS FILE -- it is overwritten on every update. +# +# HOW TO CUSTOMIZE: +# 1. Create an override file with only the fields you want to change: +# _bmad/customizations/gds-create-story.toml (team/org, committed to git) +# _bmad/customizations/gds-create-story.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/workflows/4-production/gds-create-story/scripts/resolve-customization.py b/src/workflows/4-production/gds-create-story/scripts/resolve-customization.py new file mode 100755 index 0000000..b4c6aae --- /dev/null +++ b/src/workflows/4-production/gds-create-story/scripts/resolve-customization.py @@ -0,0 +1,182 @@ +#!/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 os +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 Exception 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* looks like a ``[[menu]]`` array of tables with ``code`` keys.""" + return ( + isinstance(value, list) + and len(value) > 0 + and isinstance(value[0], dict) + and "code" in value[0] + ) + + +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} + for item in override: + 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) + 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/workflows/4-production/gds-dev-story/SKILL.md b/src/workflows/4-production/gds-dev-story/SKILL.md index 08d97a5..bb3f684 100644 --- a/src/workflows/4-production/gds-dev-story/SKILL.md +++ b/src/workflows/4-production/gds-dev-story/SKILL.md @@ -3,4 +3,20 @@ name: gds-dev-story description: 'Execute story implementation following a context filled story spec file. Use when the user says "dev this story [story file]" or "implement the next story in the sprint plan"' --- +## Resolve Customization + +Resolve `inject` and `additional_resources` from customization: +Run: `python ./scripts/resolve-customization.py gds-dev-story --key inject --key additional_resources` +Use the JSON output as resolved values. + +If `inject.before` is not empty, incorporate its content as high-priority context. +If `additional_resources` is not empty, read each listed file and incorporate as reference context. + Follow the instructions in ./workflow.md. + +## Post-Workflow Customization + +After the workflow completes, resolve `inject.after` from customization: +Run: `python ./scripts/resolve-customization.py gds-dev-story --key inject.after` + +If resolved `inject.after` is not empty, incorporate its content as a final checklist or validation gate. diff --git a/src/workflows/4-production/gds-dev-story/customize.toml b/src/workflows/4-production/gds-dev-story/customize.toml new file mode 100644 index 0000000..0762de9 --- /dev/null +++ b/src/workflows/4-production/gds-dev-story/customize.toml @@ -0,0 +1,27 @@ +# ────────────────────────────────────────────────────────────────── +# Customization Defaults: gds-dev-story +# This file defines all customizable fields for this skill. +# DO NOT EDIT THIS FILE -- it is overwritten on every update. +# +# HOW TO CUSTOMIZE: +# 1. Create an override file with only the fields you want to change: +# _bmad/customizations/gds-dev-story.toml (team/org, committed to git) +# _bmad/customizations/gds-dev-story.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/workflows/4-production/gds-dev-story/scripts/resolve-customization.py b/src/workflows/4-production/gds-dev-story/scripts/resolve-customization.py new file mode 100755 index 0000000..b4c6aae --- /dev/null +++ b/src/workflows/4-production/gds-dev-story/scripts/resolve-customization.py @@ -0,0 +1,182 @@ +#!/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 os +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 Exception 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* looks like a ``[[menu]]`` array of tables with ``code`` keys.""" + return ( + isinstance(value, list) + and len(value) > 0 + and isinstance(value[0], dict) + and "code" in value[0] + ) + + +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} + for item in override: + 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) + 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/workflows/4-production/gds-retrospective/SKILL.md b/src/workflows/4-production/gds-retrospective/SKILL.md index 2fe83f1..ce5eb53 100644 --- a/src/workflows/4-production/gds-retrospective/SKILL.md +++ b/src/workflows/4-production/gds-retrospective/SKILL.md @@ -3,4 +3,20 @@ name: gds-retrospective description: 'Facilitate a retrospective after completing a game development epic. Use when the user says "run a retrospective" or "lets do a retro"' --- +## Resolve Customization + +Resolve `inject` and `additional_resources` from customization: +Run: `python ./scripts/resolve-customization.py gds-retrospective --key inject --key additional_resources` +Use the JSON output as resolved values. + +If `inject.before` is not empty, incorporate its content as high-priority context. +If `additional_resources` is not empty, read each listed file and incorporate as reference context. + Follow the instructions in ./workflow.md. + +## Post-Workflow Customization + +After the workflow completes, resolve `inject.after` from customization: +Run: `python ./scripts/resolve-customization.py gds-retrospective --key inject.after` + +If resolved `inject.after` is not empty, incorporate its content as a final checklist or validation gate. diff --git a/src/workflows/4-production/gds-retrospective/customize.toml b/src/workflows/4-production/gds-retrospective/customize.toml new file mode 100644 index 0000000..00c0d3a --- /dev/null +++ b/src/workflows/4-production/gds-retrospective/customize.toml @@ -0,0 +1,27 @@ +# ────────────────────────────────────────────────────────────────── +# Customization Defaults: gds-retrospective +# This file defines all customizable fields for this skill. +# DO NOT EDIT THIS FILE -- it is overwritten on every update. +# +# HOW TO CUSTOMIZE: +# 1. Create an override file with only the fields you want to change: +# _bmad/customizations/gds-retrospective.toml (team/org, committed to git) +# _bmad/customizations/gds-retrospective.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/workflows/4-production/gds-retrospective/scripts/resolve-customization.py b/src/workflows/4-production/gds-retrospective/scripts/resolve-customization.py new file mode 100755 index 0000000..b4c6aae --- /dev/null +++ b/src/workflows/4-production/gds-retrospective/scripts/resolve-customization.py @@ -0,0 +1,182 @@ +#!/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 os +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 Exception 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* looks like a ``[[menu]]`` array of tables with ``code`` keys.""" + return ( + isinstance(value, list) + and len(value) > 0 + and isinstance(value[0], dict) + and "code" in value[0] + ) + + +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} + for item in override: + 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) + 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/workflows/4-production/gds-sprint-planning/SKILL.md b/src/workflows/4-production/gds-sprint-planning/SKILL.md index 6cb3475..9fb1956 100644 --- a/src/workflows/4-production/gds-sprint-planning/SKILL.md +++ b/src/workflows/4-production/gds-sprint-planning/SKILL.md @@ -3,4 +3,20 @@ name: gds-sprint-planning description: 'Generate or update sprint status from epic files for sprint planning. Use when the user says "plan the sprint" or "create sprint plan"' --- +## Resolve Customization + +Resolve `inject` and `additional_resources` from customization: +Run: `python ./scripts/resolve-customization.py gds-sprint-planning --key inject --key additional_resources` +Use the JSON output as resolved values. + +If `inject.before` is not empty, incorporate its content as high-priority context. +If `additional_resources` is not empty, read each listed file and incorporate as reference context. + Follow the instructions in ./workflow.md. + +## Post-Workflow Customization + +After the workflow completes, resolve `inject.after` from customization: +Run: `python ./scripts/resolve-customization.py gds-sprint-planning --key inject.after` + +If resolved `inject.after` is not empty, incorporate its content as a final checklist or validation gate. diff --git a/src/workflows/4-production/gds-sprint-planning/customize.toml b/src/workflows/4-production/gds-sprint-planning/customize.toml new file mode 100644 index 0000000..a14e032 --- /dev/null +++ b/src/workflows/4-production/gds-sprint-planning/customize.toml @@ -0,0 +1,27 @@ +# ────────────────────────────────────────────────────────────────── +# Customization Defaults: gds-sprint-planning +# This file defines all customizable fields for this skill. +# DO NOT EDIT THIS FILE -- it is overwritten on every update. +# +# HOW TO CUSTOMIZE: +# 1. Create an override file with only the fields you want to change: +# _bmad/customizations/gds-sprint-planning.toml (team/org, committed to git) +# _bmad/customizations/gds-sprint-planning.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/workflows/4-production/gds-sprint-planning/scripts/resolve-customization.py b/src/workflows/4-production/gds-sprint-planning/scripts/resolve-customization.py new file mode 100755 index 0000000..b4c6aae --- /dev/null +++ b/src/workflows/4-production/gds-sprint-planning/scripts/resolve-customization.py @@ -0,0 +1,182 @@ +#!/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 os +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 Exception 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* looks like a ``[[menu]]`` array of tables with ``code`` keys.""" + return ( + isinstance(value, list) + and len(value) > 0 + and isinstance(value[0], dict) + and "code" in value[0] + ) + + +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} + for item in override: + 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) + 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/workflows/4-production/gds-sprint-status/SKILL.md b/src/workflows/4-production/gds-sprint-status/SKILL.md index a96fedc..fb867b9 100644 --- a/src/workflows/4-production/gds-sprint-status/SKILL.md +++ b/src/workflows/4-production/gds-sprint-status/SKILL.md @@ -3,4 +3,20 @@ name: gds-sprint-status description: 'Summarize current sprint progress and surface risks. Use when the user says "sprint status" or "how is the sprint going"' --- +## Resolve Customization + +Resolve `inject` and `additional_resources` from customization: +Run: `python ./scripts/resolve-customization.py gds-sprint-status --key inject --key additional_resources` +Use the JSON output as resolved values. + +If `inject.before` is not empty, incorporate its content as high-priority context. +If `additional_resources` is not empty, read each listed file and incorporate as reference context. + Follow the instructions in ./workflow.md. + +## Post-Workflow Customization + +After the workflow completes, resolve `inject.after` from customization: +Run: `python ./scripts/resolve-customization.py gds-sprint-status --key inject.after` + +If resolved `inject.after` is not empty, incorporate its content as a final checklist or validation gate. diff --git a/src/workflows/4-production/gds-sprint-status/customize.toml b/src/workflows/4-production/gds-sprint-status/customize.toml new file mode 100644 index 0000000..55fd21e --- /dev/null +++ b/src/workflows/4-production/gds-sprint-status/customize.toml @@ -0,0 +1,27 @@ +# ────────────────────────────────────────────────────────────────── +# Customization Defaults: gds-sprint-status +# This file defines all customizable fields for this skill. +# DO NOT EDIT THIS FILE -- it is overwritten on every update. +# +# HOW TO CUSTOMIZE: +# 1. Create an override file with only the fields you want to change: +# _bmad/customizations/gds-sprint-status.toml (team/org, committed to git) +# _bmad/customizations/gds-sprint-status.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/workflows/4-production/gds-sprint-status/scripts/resolve-customization.py b/src/workflows/4-production/gds-sprint-status/scripts/resolve-customization.py new file mode 100755 index 0000000..b4c6aae --- /dev/null +++ b/src/workflows/4-production/gds-sprint-status/scripts/resolve-customization.py @@ -0,0 +1,182 @@ +#!/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 os +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 Exception 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* looks like a ``[[menu]]`` array of tables with ``code`` keys.""" + return ( + isinstance(value, list) + and len(value) > 0 + and isinstance(value[0], dict) + and "code" in value[0] + ) + + +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} + for item in override: + 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) + 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/workflows/gametest/gds-e2e-scaffold/SKILL.md b/src/workflows/gametest/gds-e2e-scaffold/SKILL.md index 27306a7..0b1ef6b 100644 --- a/src/workflows/gametest/gds-e2e-scaffold/SKILL.md +++ b/src/workflows/gametest/gds-e2e-scaffold/SKILL.md @@ -3,4 +3,20 @@ name: gds-e2e-scaffold description: 'Scaffold end-to-end testing infrastructure. Use when the user says "e2e scaffold" or "set up e2e testing"' --- +## Resolve Customization + +Resolve `inject` and `additional_resources` from customization: +Run: `python ./scripts/resolve-customization.py gds-e2e-scaffold --key inject --key additional_resources` +Use the JSON output as resolved values. + +If `inject.before` is not empty, incorporate its content as high-priority context. +If `additional_resources` is not empty, read each listed file and incorporate as reference context. + Follow the instructions in ./workflow.md. + +## Post-Workflow Customization + +After the workflow completes, resolve `inject.after` from customization: +Run: `python ./scripts/resolve-customization.py gds-e2e-scaffold --key inject.after` + +If resolved `inject.after` is not empty, incorporate its content as a final checklist or validation gate. diff --git a/src/workflows/gametest/gds-e2e-scaffold/customize.toml b/src/workflows/gametest/gds-e2e-scaffold/customize.toml new file mode 100644 index 0000000..42dfce6 --- /dev/null +++ b/src/workflows/gametest/gds-e2e-scaffold/customize.toml @@ -0,0 +1,27 @@ +# ────────────────────────────────────────────────────────────────── +# Customization Defaults: gds-e2e-scaffold +# This file defines all customizable fields for this skill. +# DO NOT EDIT THIS FILE -- it is overwritten on every update. +# +# HOW TO CUSTOMIZE: +# 1. Create an override file with only the fields you want to change: +# _bmad/customizations/gds-e2e-scaffold.toml (team/org, committed to git) +# _bmad/customizations/gds-e2e-scaffold.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/workflows/gametest/gds-e2e-scaffold/scripts/resolve-customization.py b/src/workflows/gametest/gds-e2e-scaffold/scripts/resolve-customization.py new file mode 100755 index 0000000..b4c6aae --- /dev/null +++ b/src/workflows/gametest/gds-e2e-scaffold/scripts/resolve-customization.py @@ -0,0 +1,182 @@ +#!/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 os +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 Exception 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* looks like a ``[[menu]]`` array of tables with ``code`` keys.""" + return ( + isinstance(value, list) + and len(value) > 0 + and isinstance(value[0], dict) + and "code" in value[0] + ) + + +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} + for item in override: + 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) + 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/workflows/gametest/gds-performance-test/SKILL.md b/src/workflows/gametest/gds-performance-test/SKILL.md index f6a0f31..5ae6ef1 100644 --- a/src/workflows/gametest/gds-performance-test/SKILL.md +++ b/src/workflows/gametest/gds-performance-test/SKILL.md @@ -3,4 +3,20 @@ name: gds-performance-test description: 'Design game performance testing strategy. Use when the user says "performance test" or "benchmark"' --- +## Resolve Customization + +Resolve `inject` and `additional_resources` from customization: +Run: `python ./scripts/resolve-customization.py gds-performance-test --key inject --key additional_resources` +Use the JSON output as resolved values. + +If `inject.before` is not empty, incorporate its content as high-priority context. +If `additional_resources` is not empty, read each listed file and incorporate as reference context. + Follow the instructions in ./workflow.md. + +## Post-Workflow Customization + +After the workflow completes, resolve `inject.after` from customization: +Run: `python ./scripts/resolve-customization.py gds-performance-test --key inject.after` + +If resolved `inject.after` is not empty, incorporate its content as a final checklist or validation gate. diff --git a/src/workflows/gametest/gds-performance-test/customize.toml b/src/workflows/gametest/gds-performance-test/customize.toml new file mode 100644 index 0000000..13aa6f9 --- /dev/null +++ b/src/workflows/gametest/gds-performance-test/customize.toml @@ -0,0 +1,27 @@ +# ────────────────────────────────────────────────────────────────── +# Customization Defaults: gds-performance-test +# This file defines all customizable fields for this skill. +# DO NOT EDIT THIS FILE -- it is overwritten on every update. +# +# HOW TO CUSTOMIZE: +# 1. Create an override file with only the fields you want to change: +# _bmad/customizations/gds-performance-test.toml (team/org, committed to git) +# _bmad/customizations/gds-performance-test.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/workflows/gametest/gds-performance-test/scripts/resolve-customization.py b/src/workflows/gametest/gds-performance-test/scripts/resolve-customization.py new file mode 100755 index 0000000..b4c6aae --- /dev/null +++ b/src/workflows/gametest/gds-performance-test/scripts/resolve-customization.py @@ -0,0 +1,182 @@ +#!/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 os +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 Exception 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* looks like a ``[[menu]]`` array of tables with ``code`` keys.""" + return ( + isinstance(value, list) + and len(value) > 0 + and isinstance(value[0], dict) + and "code" in value[0] + ) + + +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} + for item in override: + 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) + 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/workflows/gametest/gds-playtest-plan/SKILL.md b/src/workflows/gametest/gds-playtest-plan/SKILL.md index 4488e01..112a5c2 100644 --- a/src/workflows/gametest/gds-playtest-plan/SKILL.md +++ b/src/workflows/gametest/gds-playtest-plan/SKILL.md @@ -3,4 +3,20 @@ name: gds-playtest-plan description: 'Create structured playtesting plans for user feedback. Use when the user says "playtest plan" or "playtesting"' --- +## Resolve Customization + +Resolve `inject` and `additional_resources` from customization: +Run: `python ./scripts/resolve-customization.py gds-playtest-plan --key inject --key additional_resources` +Use the JSON output as resolved values. + +If `inject.before` is not empty, incorporate its content as high-priority context. +If `additional_resources` is not empty, read each listed file and incorporate as reference context. + Follow the instructions in ./workflow.md. + +## Post-Workflow Customization + +After the workflow completes, resolve `inject.after` from customization: +Run: `python ./scripts/resolve-customization.py gds-playtest-plan --key inject.after` + +If resolved `inject.after` is not empty, incorporate its content as a final checklist or validation gate. diff --git a/src/workflows/gametest/gds-playtest-plan/customize.toml b/src/workflows/gametest/gds-playtest-plan/customize.toml new file mode 100644 index 0000000..e3bdf3a --- /dev/null +++ b/src/workflows/gametest/gds-playtest-plan/customize.toml @@ -0,0 +1,27 @@ +# ────────────────────────────────────────────────────────────────── +# Customization Defaults: gds-playtest-plan +# This file defines all customizable fields for this skill. +# DO NOT EDIT THIS FILE -- it is overwritten on every update. +# +# HOW TO CUSTOMIZE: +# 1. Create an override file with only the fields you want to change: +# _bmad/customizations/gds-playtest-plan.toml (team/org, committed to git) +# _bmad/customizations/gds-playtest-plan.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/workflows/gametest/gds-playtest-plan/scripts/resolve-customization.py b/src/workflows/gametest/gds-playtest-plan/scripts/resolve-customization.py new file mode 100755 index 0000000..b4c6aae --- /dev/null +++ b/src/workflows/gametest/gds-playtest-plan/scripts/resolve-customization.py @@ -0,0 +1,182 @@ +#!/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 os +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 Exception 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* looks like a ``[[menu]]`` array of tables with ``code`` keys.""" + return ( + isinstance(value, list) + and len(value) > 0 + and isinstance(value[0], dict) + and "code" in value[0] + ) + + +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} + for item in override: + 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) + 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/workflows/gametest/gds-test-automate/SKILL.md b/src/workflows/gametest/gds-test-automate/SKILL.md index a46115e..fa0dfc1 100644 --- a/src/workflows/gametest/gds-test-automate/SKILL.md +++ b/src/workflows/gametest/gds-test-automate/SKILL.md @@ -3,4 +3,20 @@ name: gds-test-automate description: 'Generate automated game tests for gameplay systems. Use when the user says "automate tests" or "generate tests"' --- +## Resolve Customization + +Resolve `inject` and `additional_resources` from customization: +Run: `python ./scripts/resolve-customization.py gds-test-automate --key inject --key additional_resources` +Use the JSON output as resolved values. + +If `inject.before` is not empty, incorporate its content as high-priority context. +If `additional_resources` is not empty, read each listed file and incorporate as reference context. + Follow the instructions in ./workflow.md. + +## Post-Workflow Customization + +After the workflow completes, resolve `inject.after` from customization: +Run: `python ./scripts/resolve-customization.py gds-test-automate --key inject.after` + +If resolved `inject.after` is not empty, incorporate its content as a final checklist or validation gate. diff --git a/src/workflows/gametest/gds-test-automate/customize.toml b/src/workflows/gametest/gds-test-automate/customize.toml new file mode 100644 index 0000000..593ddcb --- /dev/null +++ b/src/workflows/gametest/gds-test-automate/customize.toml @@ -0,0 +1,27 @@ +# ────────────────────────────────────────────────────────────────── +# Customization Defaults: gds-test-automate +# This file defines all customizable fields for this skill. +# DO NOT EDIT THIS FILE -- it is overwritten on every update. +# +# HOW TO CUSTOMIZE: +# 1. Create an override file with only the fields you want to change: +# _bmad/customizations/gds-test-automate.toml (team/org, committed to git) +# _bmad/customizations/gds-test-automate.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/workflows/gametest/gds-test-automate/scripts/resolve-customization.py b/src/workflows/gametest/gds-test-automate/scripts/resolve-customization.py new file mode 100755 index 0000000..b4c6aae --- /dev/null +++ b/src/workflows/gametest/gds-test-automate/scripts/resolve-customization.py @@ -0,0 +1,182 @@ +#!/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 os +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 Exception 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* looks like a ``[[menu]]`` array of tables with ``code`` keys.""" + return ( + isinstance(value, list) + and len(value) > 0 + and isinstance(value[0], dict) + and "code" in value[0] + ) + + +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} + for item in override: + 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) + 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/workflows/gametest/gds-test-design/SKILL.md b/src/workflows/gametest/gds-test-design/SKILL.md index 40ee993..2960fae 100644 --- a/src/workflows/gametest/gds-test-design/SKILL.md +++ b/src/workflows/gametest/gds-test-design/SKILL.md @@ -3,4 +3,20 @@ name: gds-test-design description: 'Create comprehensive game test scenarios. Use when the user says "test design" or "design tests"' --- +## Resolve Customization + +Resolve `inject` and `additional_resources` from customization: +Run: `python ./scripts/resolve-customization.py gds-test-design --key inject --key additional_resources` +Use the JSON output as resolved values. + +If `inject.before` is not empty, incorporate its content as high-priority context. +If `additional_resources` is not empty, read each listed file and incorporate as reference context. + Follow the instructions in ./workflow.md. + +## Post-Workflow Customization + +After the workflow completes, resolve `inject.after` from customization: +Run: `python ./scripts/resolve-customization.py gds-test-design --key inject.after` + +If resolved `inject.after` is not empty, incorporate its content as a final checklist or validation gate. diff --git a/src/workflows/gametest/gds-test-design/customize.toml b/src/workflows/gametest/gds-test-design/customize.toml new file mode 100644 index 0000000..06ce052 --- /dev/null +++ b/src/workflows/gametest/gds-test-design/customize.toml @@ -0,0 +1,27 @@ +# ────────────────────────────────────────────────────────────────── +# Customization Defaults: gds-test-design +# This file defines all customizable fields for this skill. +# DO NOT EDIT THIS FILE -- it is overwritten on every update. +# +# HOW TO CUSTOMIZE: +# 1. Create an override file with only the fields you want to change: +# _bmad/customizations/gds-test-design.toml (team/org, committed to git) +# _bmad/customizations/gds-test-design.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/workflows/gametest/gds-test-design/scripts/resolve-customization.py b/src/workflows/gametest/gds-test-design/scripts/resolve-customization.py new file mode 100755 index 0000000..b4c6aae --- /dev/null +++ b/src/workflows/gametest/gds-test-design/scripts/resolve-customization.py @@ -0,0 +1,182 @@ +#!/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 os +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 Exception 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* looks like a ``[[menu]]`` array of tables with ``code`` keys.""" + return ( + isinstance(value, list) + and len(value) > 0 + and isinstance(value[0], dict) + and "code" in value[0] + ) + + +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} + for item in override: + 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) + 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/workflows/gametest/gds-test-framework/SKILL.md b/src/workflows/gametest/gds-test-framework/SKILL.md index 7e6271b..022b9d6 100644 --- a/src/workflows/gametest/gds-test-framework/SKILL.md +++ b/src/workflows/gametest/gds-test-framework/SKILL.md @@ -3,4 +3,20 @@ name: gds-test-framework description: 'Initialize game test framework for Unity, Unreal, or Godot. Use when the user says "test framework" or "set up testing"' --- +## Resolve Customization + +Resolve `inject` and `additional_resources` from customization: +Run: `python ./scripts/resolve-customization.py gds-test-framework --key inject --key additional_resources` +Use the JSON output as resolved values. + +If `inject.before` is not empty, incorporate its content as high-priority context. +If `additional_resources` is not empty, read each listed file and incorporate as reference context. + Follow the instructions in ./workflow.md. + +## Post-Workflow Customization + +After the workflow completes, resolve `inject.after` from customization: +Run: `python ./scripts/resolve-customization.py gds-test-framework --key inject.after` + +If resolved `inject.after` is not empty, incorporate its content as a final checklist or validation gate. diff --git a/src/workflows/gametest/gds-test-framework/customize.toml b/src/workflows/gametest/gds-test-framework/customize.toml new file mode 100644 index 0000000..dda9285 --- /dev/null +++ b/src/workflows/gametest/gds-test-framework/customize.toml @@ -0,0 +1,27 @@ +# ────────────────────────────────────────────────────────────────── +# Customization Defaults: gds-test-framework +# This file defines all customizable fields for this skill. +# DO NOT EDIT THIS FILE -- it is overwritten on every update. +# +# HOW TO CUSTOMIZE: +# 1. Create an override file with only the fields you want to change: +# _bmad/customizations/gds-test-framework.toml (team/org, committed to git) +# _bmad/customizations/gds-test-framework.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/workflows/gametest/gds-test-framework/scripts/resolve-customization.py b/src/workflows/gametest/gds-test-framework/scripts/resolve-customization.py new file mode 100755 index 0000000..b4c6aae --- /dev/null +++ b/src/workflows/gametest/gds-test-framework/scripts/resolve-customization.py @@ -0,0 +1,182 @@ +#!/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 os +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 Exception 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* looks like a ``[[menu]]`` array of tables with ``code`` keys.""" + return ( + isinstance(value, list) + and len(value) > 0 + and isinstance(value[0], dict) + and "code" in value[0] + ) + + +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} + for item in override: + 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) + 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/workflows/gametest/gds-test-review/SKILL.md b/src/workflows/gametest/gds-test-review/SKILL.md index 023ae74..243ab5c 100644 --- a/src/workflows/gametest/gds-test-review/SKILL.md +++ b/src/workflows/gametest/gds-test-review/SKILL.md @@ -3,4 +3,20 @@ name: gds-test-review description: 'Review test quality and coverage. Use when the user says "test review" or "review tests"' --- +## Resolve Customization + +Resolve `inject` and `additional_resources` from customization: +Run: `python ./scripts/resolve-customization.py gds-test-review --key inject --key additional_resources` +Use the JSON output as resolved values. + +If `inject.before` is not empty, incorporate its content as high-priority context. +If `additional_resources` is not empty, read each listed file and incorporate as reference context. + Follow the instructions in ./workflow.md. + +## Post-Workflow Customization + +After the workflow completes, resolve `inject.after` from customization: +Run: `python ./scripts/resolve-customization.py gds-test-review --key inject.after` + +If resolved `inject.after` is not empty, incorporate its content as a final checklist or validation gate. diff --git a/src/workflows/gametest/gds-test-review/customize.toml b/src/workflows/gametest/gds-test-review/customize.toml new file mode 100644 index 0000000..e81aa8d --- /dev/null +++ b/src/workflows/gametest/gds-test-review/customize.toml @@ -0,0 +1,27 @@ +# ────────────────────────────────────────────────────────────────── +# Customization Defaults: gds-test-review +# This file defines all customizable fields for this skill. +# DO NOT EDIT THIS FILE -- it is overwritten on every update. +# +# HOW TO CUSTOMIZE: +# 1. Create an override file with only the fields you want to change: +# _bmad/customizations/gds-test-review.toml (team/org, committed to git) +# _bmad/customizations/gds-test-review.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/workflows/gametest/gds-test-review/scripts/resolve-customization.py b/src/workflows/gametest/gds-test-review/scripts/resolve-customization.py new file mode 100755 index 0000000..b4c6aae --- /dev/null +++ b/src/workflows/gametest/gds-test-review/scripts/resolve-customization.py @@ -0,0 +1,182 @@ +#!/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 os +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 Exception 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* looks like a ``[[menu]]`` array of tables with ``code`` keys.""" + return ( + isinstance(value, list) + and len(value) > 0 + and isinstance(value[0], dict) + and "code" in value[0] + ) + + +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} + for item in override: + 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) + 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/workflows/gds-document-project/SKILL.md b/src/workflows/gds-document-project/SKILL.md index 574554e..3f6e966 100644 --- a/src/workflows/gds-document-project/SKILL.md +++ b/src/workflows/gds-document-project/SKILL.md @@ -3,4 +3,20 @@ name: gds-document-project description: 'Analyze existing game projects to produce useful documentation. Use when the user says "document project" or "generate docs"' --- +## Resolve Customization + +Resolve `inject` and `additional_resources` from customization: +Run: `python ./scripts/resolve-customization.py gds-document-project --key inject --key additional_resources` +Use the JSON output as resolved values. + +If `inject.before` is not empty, incorporate its content as high-priority context. +If `additional_resources` is not empty, read each listed file and incorporate as reference context. + Follow the instructions in ./workflow.md. + +## Post-Workflow Customization + +After the workflow completes, resolve `inject.after` from customization: +Run: `python ./scripts/resolve-customization.py gds-document-project --key inject.after` + +If resolved `inject.after` is not empty, incorporate its content as a final checklist or validation gate. diff --git a/src/workflows/gds-document-project/customize.toml b/src/workflows/gds-document-project/customize.toml new file mode 100644 index 0000000..bb38a72 --- /dev/null +++ b/src/workflows/gds-document-project/customize.toml @@ -0,0 +1,27 @@ +# ────────────────────────────────────────────────────────────────── +# Customization Defaults: gds-document-project +# This file defines all customizable fields for this skill. +# DO NOT EDIT THIS FILE -- it is overwritten on every update. +# +# HOW TO CUSTOMIZE: +# 1. Create an override file with only the fields you want to change: +# _bmad/customizations/gds-document-project.toml (team/org, committed to git) +# _bmad/customizations/gds-document-project.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/workflows/gds-document-project/scripts/resolve-customization.py b/src/workflows/gds-document-project/scripts/resolve-customization.py new file mode 100755 index 0000000..b4c6aae --- /dev/null +++ b/src/workflows/gds-document-project/scripts/resolve-customization.py @@ -0,0 +1,182 @@ +#!/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 os +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 Exception 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* looks like a ``[[menu]]`` array of tables with ``code`` keys.""" + return ( + isinstance(value, list) + and len(value) > 0 + and isinstance(value[0], dict) + and "code" in value[0] + ) + + +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} + for item in override: + 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) + 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/workflows/gds-quick-flow/gds-quick-dev-new-preview/SKILL.md b/src/workflows/gds-quick-flow/gds-quick-dev-new-preview/SKILL.md index 9631613..e549e23 100644 --- a/src/workflows/gds-quick-flow/gds-quick-dev-new-preview/SKILL.md +++ b/src/workflows/gds-quick-flow/gds-quick-dev-new-preview/SKILL.md @@ -3,4 +3,20 @@ name: gds-quick-dev-new-preview description: 'Implements any user intent, GDD requirement, story, bug fix or change request by producing clean working code artifacts that follow the project''s existing game architecture, patterns and conventions. Use when the user wants to build, fix, tweak, refactor, add or modify any game code, component or feature.' --- +## Resolve Customization + +Resolve `inject` and `additional_resources` from customization: +Run: `python ./scripts/resolve-customization.py gds-quick-dev-new-preview --key inject --key additional_resources` +Use the JSON output as resolved values. + +If `inject.before` is not empty, incorporate its content as high-priority context. +If `additional_resources` is not empty, read each listed file and incorporate as reference context. + Follow the instructions in ./workflow.md. + +## Post-Workflow Customization + +After the workflow completes, resolve `inject.after` from customization: +Run: `python ./scripts/resolve-customization.py gds-quick-dev-new-preview --key inject.after` + +If resolved `inject.after` is not empty, incorporate its content as a final checklist or validation gate. diff --git a/src/workflows/gds-quick-flow/gds-quick-dev-new-preview/customize.toml b/src/workflows/gds-quick-flow/gds-quick-dev-new-preview/customize.toml new file mode 100644 index 0000000..937d56a --- /dev/null +++ b/src/workflows/gds-quick-flow/gds-quick-dev-new-preview/customize.toml @@ -0,0 +1,27 @@ +# ────────────────────────────────────────────────────────────────── +# Customization Defaults: gds-quick-dev-new-preview +# This file defines all customizable fields for this skill. +# DO NOT EDIT THIS FILE -- it is overwritten on every update. +# +# HOW TO CUSTOMIZE: +# 1. Create an override file with only the fields you want to change: +# _bmad/customizations/gds-quick-dev-new-preview.toml (team/org, committed to git) +# _bmad/customizations/gds-quick-dev-new-preview.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/workflows/gds-quick-flow/gds-quick-dev-new-preview/scripts/resolve-customization.py b/src/workflows/gds-quick-flow/gds-quick-dev-new-preview/scripts/resolve-customization.py new file mode 100755 index 0000000..b4c6aae --- /dev/null +++ b/src/workflows/gds-quick-flow/gds-quick-dev-new-preview/scripts/resolve-customization.py @@ -0,0 +1,182 @@ +#!/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 os +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 Exception 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* looks like a ``[[menu]]`` array of tables with ``code`` keys.""" + return ( + isinstance(value, list) + and len(value) > 0 + and isinstance(value[0], dict) + and "code" in value[0] + ) + + +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} + for item in override: + 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) + 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/workflows/gds-quick-flow/gds-quick-dev/SKILL.md b/src/workflows/gds-quick-flow/gds-quick-dev/SKILL.md index de6c894..4d20c03 100644 --- a/src/workflows/gds-quick-flow/gds-quick-dev/SKILL.md +++ b/src/workflows/gds-quick-flow/gds-quick-dev/SKILL.md @@ -3,4 +3,20 @@ name: gds-quick-dev description: 'Flexible development workflow - execute tech-specs OR direct instructions with optional planning. Use when the user says "lets implement this feature" or "execute these development tasks"' --- +## Resolve Customization + +Resolve `inject` and `additional_resources` from customization: +Run: `python ./scripts/resolve-customization.py gds-quick-dev --key inject --key additional_resources` +Use the JSON output as resolved values. + +If `inject.before` is not empty, incorporate its content as high-priority context. +If `additional_resources` is not empty, read each listed file and incorporate as reference context. + Follow the instructions in ./workflow.md. + +## Post-Workflow Customization + +After the workflow completes, resolve `inject.after` from customization: +Run: `python ./scripts/resolve-customization.py gds-quick-dev --key inject.after` + +If resolved `inject.after` is not empty, incorporate its content as a final checklist or validation gate. diff --git a/src/workflows/gds-quick-flow/gds-quick-dev/customize.toml b/src/workflows/gds-quick-flow/gds-quick-dev/customize.toml new file mode 100644 index 0000000..ad26fee --- /dev/null +++ b/src/workflows/gds-quick-flow/gds-quick-dev/customize.toml @@ -0,0 +1,27 @@ +# ────────────────────────────────────────────────────────────────── +# Customization Defaults: gds-quick-dev +# This file defines all customizable fields for this skill. +# DO NOT EDIT THIS FILE -- it is overwritten on every update. +# +# HOW TO CUSTOMIZE: +# 1. Create an override file with only the fields you want to change: +# _bmad/customizations/gds-quick-dev.toml (team/org, committed to git) +# _bmad/customizations/gds-quick-dev.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/workflows/gds-quick-flow/gds-quick-dev/scripts/resolve-customization.py b/src/workflows/gds-quick-flow/gds-quick-dev/scripts/resolve-customization.py new file mode 100755 index 0000000..b4c6aae --- /dev/null +++ b/src/workflows/gds-quick-flow/gds-quick-dev/scripts/resolve-customization.py @@ -0,0 +1,182 @@ +#!/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 os +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 Exception 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* looks like a ``[[menu]]`` array of tables with ``code`` keys.""" + return ( + isinstance(value, list) + and len(value) > 0 + and isinstance(value[0], dict) + and "code" in value[0] + ) + + +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} + for item in override: + 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) + 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/workflows/gds-quick-flow/gds-quick-spec/SKILL.md b/src/workflows/gds-quick-flow/gds-quick-spec/SKILL.md index 5a876ab..c82d639 100644 --- a/src/workflows/gds-quick-flow/gds-quick-spec/SKILL.md +++ b/src/workflows/gds-quick-flow/gds-quick-spec/SKILL.md @@ -3,4 +3,20 @@ name: gds-quick-spec description: 'Create technical specs with implementation-ready stories. Use when the user says "quick spec" or "tech spec"' --- +## Resolve Customization + +Resolve `inject` and `additional_resources` from customization: +Run: `python ./scripts/resolve-customization.py gds-quick-spec --key inject --key additional_resources` +Use the JSON output as resolved values. + +If `inject.before` is not empty, incorporate its content as high-priority context. +If `additional_resources` is not empty, read each listed file and incorporate as reference context. + Follow the instructions in ./workflow.md. + +## Post-Workflow Customization + +After the workflow completes, resolve `inject.after` from customization: +Run: `python ./scripts/resolve-customization.py gds-quick-spec --key inject.after` + +If resolved `inject.after` is not empty, incorporate its content as a final checklist or validation gate. diff --git a/src/workflows/gds-quick-flow/gds-quick-spec/customize.toml b/src/workflows/gds-quick-flow/gds-quick-spec/customize.toml new file mode 100644 index 0000000..b8a7fdd --- /dev/null +++ b/src/workflows/gds-quick-flow/gds-quick-spec/customize.toml @@ -0,0 +1,27 @@ +# ────────────────────────────────────────────────────────────────── +# Customization Defaults: gds-quick-spec +# This file defines all customizable fields for this skill. +# DO NOT EDIT THIS FILE -- it is overwritten on every update. +# +# HOW TO CUSTOMIZE: +# 1. Create an override file with only the fields you want to change: +# _bmad/customizations/gds-quick-spec.toml (team/org, committed to git) +# _bmad/customizations/gds-quick-spec.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/workflows/gds-quick-flow/gds-quick-spec/scripts/resolve-customization.py b/src/workflows/gds-quick-flow/gds-quick-spec/scripts/resolve-customization.py new file mode 100755 index 0000000..b4c6aae --- /dev/null +++ b/src/workflows/gds-quick-flow/gds-quick-spec/scripts/resolve-customization.py @@ -0,0 +1,182 @@ +#!/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 os +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 Exception 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* looks like a ``[[menu]]`` array of tables with ``code`` keys.""" + return ( + isinstance(value, list) + and len(value) > 0 + and isinstance(value[0], dict) + and "code" in value[0] + ) + + +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} + for item in override: + 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) + 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() From 25f9de3bc58cd4f027a20dbc6c3265c29b6c09a3 Mon Sep 17 00:00:00 2001 From: Brian Madison Date: Tue, 14 Apr 2026 12:04:36 -0500 Subject: [PATCH 2/7] fix: narrow exception handler in resolve-customization.py Change except Exception to except (tomllib.TOMLDecodeError, OSError) for clearer failure signaling in user-facing tooling. --- .../gds-agent-game-architect/scripts/resolve-customization.py | 2 +- .../gds-agent-game-designer/scripts/resolve-customization.py | 2 +- src/agents/gds-agent-game-dev/scripts/resolve-customization.py | 2 +- src/agents/gds-agent-game-qa/scripts/resolve-customization.py | 2 +- .../scripts/resolve-customization.py | 2 +- .../gds-agent-game-solo-dev/scripts/resolve-customization.py | 2 +- .../gds-agent-tech-writer/scripts/resolve-customization.py | 2 +- .../gds-brainstorm-game/scripts/resolve-customization.py | 2 +- .../gds-create-game-brief/scripts/resolve-customization.py | 2 +- .../gds-domain-research/scripts/resolve-customization.py | 2 +- .../2-design/gds-create-gdd/scripts/resolve-customization.py | 2 +- .../gds-create-narrative/scripts/resolve-customization.py | 2 +- .../gds-create-ux-design/scripts/resolve-customization.py | 2 +- .../scripts/resolve-customization.py | 2 +- .../scripts/resolve-customization.py | 2 +- .../gds-game-architecture/scripts/resolve-customization.py | 2 +- .../scripts/resolve-customization.py | 2 +- .../gds-code-review/scripts/resolve-customization.py | 2 +- .../gds-correct-course/scripts/resolve-customization.py | 2 +- .../gds-create-story/scripts/resolve-customization.py | 2 +- .../4-production/gds-dev-story/scripts/resolve-customization.py | 2 +- .../gds-retrospective/scripts/resolve-customization.py | 2 +- .../gds-sprint-planning/scripts/resolve-customization.py | 2 +- .../gds-sprint-status/scripts/resolve-customization.py | 2 +- .../gametest/gds-e2e-scaffold/scripts/resolve-customization.py | 2 +- .../gds-performance-test/scripts/resolve-customization.py | 2 +- .../gametest/gds-playtest-plan/scripts/resolve-customization.py | 2 +- .../gametest/gds-test-automate/scripts/resolve-customization.py | 2 +- .../gametest/gds-test-design/scripts/resolve-customization.py | 2 +- .../gds-test-framework/scripts/resolve-customization.py | 2 +- .../gametest/gds-test-review/scripts/resolve-customization.py | 2 +- .../gds-document-project/scripts/resolve-customization.py | 2 +- .../gds-quick-dev-new-preview/scripts/resolve-customization.py | 2 +- .../gds-quick-dev/scripts/resolve-customization.py | 2 +- .../gds-quick-spec/scripts/resolve-customization.py | 2 +- 35 files changed, 35 insertions(+), 35 deletions(-) diff --git a/src/agents/gds-agent-game-architect/scripts/resolve-customization.py b/src/agents/gds-agent-game-architect/scripts/resolve-customization.py index b4c6aae..6bbce66 100755 --- a/src/agents/gds-agent-game-architect/scripts/resolve-customization.py +++ b/src/agents/gds-agent-game-architect/scripts/resolve-customization.py @@ -47,7 +47,7 @@ def load_toml(path: Path) -> dict[str, Any]: try: with open(path, "rb") as f: return tomllib.load(f) - except Exception as exc: + except (tomllib.TOMLDecodeError, OSError) as exc: print(f"warning: failed to parse {path}: {exc}", file=sys.stderr) return {} diff --git a/src/agents/gds-agent-game-designer/scripts/resolve-customization.py b/src/agents/gds-agent-game-designer/scripts/resolve-customization.py index b4c6aae..6bbce66 100755 --- a/src/agents/gds-agent-game-designer/scripts/resolve-customization.py +++ b/src/agents/gds-agent-game-designer/scripts/resolve-customization.py @@ -47,7 +47,7 @@ def load_toml(path: Path) -> dict[str, Any]: try: with open(path, "rb") as f: return tomllib.load(f) - except Exception as exc: + except (tomllib.TOMLDecodeError, OSError) as exc: print(f"warning: failed to parse {path}: {exc}", file=sys.stderr) return {} diff --git a/src/agents/gds-agent-game-dev/scripts/resolve-customization.py b/src/agents/gds-agent-game-dev/scripts/resolve-customization.py index b4c6aae..6bbce66 100755 --- a/src/agents/gds-agent-game-dev/scripts/resolve-customization.py +++ b/src/agents/gds-agent-game-dev/scripts/resolve-customization.py @@ -47,7 +47,7 @@ def load_toml(path: Path) -> dict[str, Any]: try: with open(path, "rb") as f: return tomllib.load(f) - except Exception as exc: + except (tomllib.TOMLDecodeError, OSError) as exc: print(f"warning: failed to parse {path}: {exc}", file=sys.stderr) return {} diff --git a/src/agents/gds-agent-game-qa/scripts/resolve-customization.py b/src/agents/gds-agent-game-qa/scripts/resolve-customization.py index b4c6aae..6bbce66 100755 --- a/src/agents/gds-agent-game-qa/scripts/resolve-customization.py +++ b/src/agents/gds-agent-game-qa/scripts/resolve-customization.py @@ -47,7 +47,7 @@ def load_toml(path: Path) -> dict[str, Any]: try: with open(path, "rb") as f: return tomllib.load(f) - except Exception as exc: + except (tomllib.TOMLDecodeError, OSError) as exc: print(f"warning: failed to parse {path}: {exc}", file=sys.stderr) return {} diff --git a/src/agents/gds-agent-game-scrum-master/scripts/resolve-customization.py b/src/agents/gds-agent-game-scrum-master/scripts/resolve-customization.py index b4c6aae..6bbce66 100755 --- a/src/agents/gds-agent-game-scrum-master/scripts/resolve-customization.py +++ b/src/agents/gds-agent-game-scrum-master/scripts/resolve-customization.py @@ -47,7 +47,7 @@ def load_toml(path: Path) -> dict[str, Any]: try: with open(path, "rb") as f: return tomllib.load(f) - except Exception as exc: + except (tomllib.TOMLDecodeError, OSError) as exc: print(f"warning: failed to parse {path}: {exc}", file=sys.stderr) return {} diff --git a/src/agents/gds-agent-game-solo-dev/scripts/resolve-customization.py b/src/agents/gds-agent-game-solo-dev/scripts/resolve-customization.py index b4c6aae..6bbce66 100755 --- a/src/agents/gds-agent-game-solo-dev/scripts/resolve-customization.py +++ b/src/agents/gds-agent-game-solo-dev/scripts/resolve-customization.py @@ -47,7 +47,7 @@ def load_toml(path: Path) -> dict[str, Any]: try: with open(path, "rb") as f: return tomllib.load(f) - except Exception as exc: + except (tomllib.TOMLDecodeError, OSError) as exc: print(f"warning: failed to parse {path}: {exc}", file=sys.stderr) return {} diff --git a/src/agents/gds-agent-tech-writer/scripts/resolve-customization.py b/src/agents/gds-agent-tech-writer/scripts/resolve-customization.py index b4c6aae..6bbce66 100755 --- a/src/agents/gds-agent-tech-writer/scripts/resolve-customization.py +++ b/src/agents/gds-agent-tech-writer/scripts/resolve-customization.py @@ -47,7 +47,7 @@ def load_toml(path: Path) -> dict[str, Any]: try: with open(path, "rb") as f: return tomllib.load(f) - except Exception as exc: + except (tomllib.TOMLDecodeError, OSError) as exc: print(f"warning: failed to parse {path}: {exc}", file=sys.stderr) return {} diff --git a/src/workflows/1-preproduction/gds-brainstorm-game/scripts/resolve-customization.py b/src/workflows/1-preproduction/gds-brainstorm-game/scripts/resolve-customization.py index b4c6aae..6bbce66 100755 --- a/src/workflows/1-preproduction/gds-brainstorm-game/scripts/resolve-customization.py +++ b/src/workflows/1-preproduction/gds-brainstorm-game/scripts/resolve-customization.py @@ -47,7 +47,7 @@ def load_toml(path: Path) -> dict[str, Any]: try: with open(path, "rb") as f: return tomllib.load(f) - except Exception as exc: + except (tomllib.TOMLDecodeError, OSError) as exc: print(f"warning: failed to parse {path}: {exc}", file=sys.stderr) return {} diff --git a/src/workflows/1-preproduction/gds-create-game-brief/scripts/resolve-customization.py b/src/workflows/1-preproduction/gds-create-game-brief/scripts/resolve-customization.py index b4c6aae..6bbce66 100755 --- a/src/workflows/1-preproduction/gds-create-game-brief/scripts/resolve-customization.py +++ b/src/workflows/1-preproduction/gds-create-game-brief/scripts/resolve-customization.py @@ -47,7 +47,7 @@ def load_toml(path: Path) -> dict[str, Any]: try: with open(path, "rb") as f: return tomllib.load(f) - except Exception as exc: + except (tomllib.TOMLDecodeError, OSError) as exc: print(f"warning: failed to parse {path}: {exc}", file=sys.stderr) return {} diff --git a/src/workflows/1-preproduction/research/gds-domain-research/scripts/resolve-customization.py b/src/workflows/1-preproduction/research/gds-domain-research/scripts/resolve-customization.py index b4c6aae..6bbce66 100755 --- a/src/workflows/1-preproduction/research/gds-domain-research/scripts/resolve-customization.py +++ b/src/workflows/1-preproduction/research/gds-domain-research/scripts/resolve-customization.py @@ -47,7 +47,7 @@ def load_toml(path: Path) -> dict[str, Any]: try: with open(path, "rb") as f: return tomllib.load(f) - except Exception as exc: + except (tomllib.TOMLDecodeError, OSError) as exc: print(f"warning: failed to parse {path}: {exc}", file=sys.stderr) return {} diff --git a/src/workflows/2-design/gds-create-gdd/scripts/resolve-customization.py b/src/workflows/2-design/gds-create-gdd/scripts/resolve-customization.py index b4c6aae..6bbce66 100755 --- a/src/workflows/2-design/gds-create-gdd/scripts/resolve-customization.py +++ b/src/workflows/2-design/gds-create-gdd/scripts/resolve-customization.py @@ -47,7 +47,7 @@ def load_toml(path: Path) -> dict[str, Any]: try: with open(path, "rb") as f: return tomllib.load(f) - except Exception as exc: + except (tomllib.TOMLDecodeError, OSError) as exc: print(f"warning: failed to parse {path}: {exc}", file=sys.stderr) return {} diff --git a/src/workflows/2-design/gds-create-narrative/scripts/resolve-customization.py b/src/workflows/2-design/gds-create-narrative/scripts/resolve-customization.py index b4c6aae..6bbce66 100755 --- a/src/workflows/2-design/gds-create-narrative/scripts/resolve-customization.py +++ b/src/workflows/2-design/gds-create-narrative/scripts/resolve-customization.py @@ -47,7 +47,7 @@ def load_toml(path: Path) -> dict[str, Any]: try: with open(path, "rb") as f: return tomllib.load(f) - except Exception as exc: + except (tomllib.TOMLDecodeError, OSError) as exc: print(f"warning: failed to parse {path}: {exc}", file=sys.stderr) return {} diff --git a/src/workflows/2-design/gds-create-ux-design/scripts/resolve-customization.py b/src/workflows/2-design/gds-create-ux-design/scripts/resolve-customization.py index b4c6aae..6bbce66 100755 --- a/src/workflows/2-design/gds-create-ux-design/scripts/resolve-customization.py +++ b/src/workflows/2-design/gds-create-ux-design/scripts/resolve-customization.py @@ -47,7 +47,7 @@ def load_toml(path: Path) -> dict[str, Any]: try: with open(path, "rb") as f: return tomllib.load(f) - except Exception as exc: + except (tomllib.TOMLDecodeError, OSError) as exc: print(f"warning: failed to parse {path}: {exc}", file=sys.stderr) return {} diff --git a/src/workflows/3-technical/gds-check-implementation-readiness/scripts/resolve-customization.py b/src/workflows/3-technical/gds-check-implementation-readiness/scripts/resolve-customization.py index b4c6aae..6bbce66 100755 --- a/src/workflows/3-technical/gds-check-implementation-readiness/scripts/resolve-customization.py +++ b/src/workflows/3-technical/gds-check-implementation-readiness/scripts/resolve-customization.py @@ -47,7 +47,7 @@ def load_toml(path: Path) -> dict[str, Any]: try: with open(path, "rb") as f: return tomllib.load(f) - except Exception as exc: + except (tomllib.TOMLDecodeError, OSError) as exc: print(f"warning: failed to parse {path}: {exc}", file=sys.stderr) return {} diff --git a/src/workflows/3-technical/gds-create-epics-and-stories/scripts/resolve-customization.py b/src/workflows/3-technical/gds-create-epics-and-stories/scripts/resolve-customization.py index b4c6aae..6bbce66 100755 --- a/src/workflows/3-technical/gds-create-epics-and-stories/scripts/resolve-customization.py +++ b/src/workflows/3-technical/gds-create-epics-and-stories/scripts/resolve-customization.py @@ -47,7 +47,7 @@ def load_toml(path: Path) -> dict[str, Any]: try: with open(path, "rb") as f: return tomllib.load(f) - except Exception as exc: + except (tomllib.TOMLDecodeError, OSError) as exc: print(f"warning: failed to parse {path}: {exc}", file=sys.stderr) return {} diff --git a/src/workflows/3-technical/gds-game-architecture/scripts/resolve-customization.py b/src/workflows/3-technical/gds-game-architecture/scripts/resolve-customization.py index b4c6aae..6bbce66 100755 --- a/src/workflows/3-technical/gds-game-architecture/scripts/resolve-customization.py +++ b/src/workflows/3-technical/gds-game-architecture/scripts/resolve-customization.py @@ -47,7 +47,7 @@ def load_toml(path: Path) -> dict[str, Any]: try: with open(path, "rb") as f: return tomllib.load(f) - except Exception as exc: + except (tomllib.TOMLDecodeError, OSError) as exc: print(f"warning: failed to parse {path}: {exc}", file=sys.stderr) return {} diff --git a/src/workflows/3-technical/gds-generate-project-context/scripts/resolve-customization.py b/src/workflows/3-technical/gds-generate-project-context/scripts/resolve-customization.py index b4c6aae..6bbce66 100755 --- a/src/workflows/3-technical/gds-generate-project-context/scripts/resolve-customization.py +++ b/src/workflows/3-technical/gds-generate-project-context/scripts/resolve-customization.py @@ -47,7 +47,7 @@ def load_toml(path: Path) -> dict[str, Any]: try: with open(path, "rb") as f: return tomllib.load(f) - except Exception as exc: + except (tomllib.TOMLDecodeError, OSError) as exc: print(f"warning: failed to parse {path}: {exc}", file=sys.stderr) return {} diff --git a/src/workflows/4-production/gds-code-review/scripts/resolve-customization.py b/src/workflows/4-production/gds-code-review/scripts/resolve-customization.py index b4c6aae..6bbce66 100755 --- a/src/workflows/4-production/gds-code-review/scripts/resolve-customization.py +++ b/src/workflows/4-production/gds-code-review/scripts/resolve-customization.py @@ -47,7 +47,7 @@ def load_toml(path: Path) -> dict[str, Any]: try: with open(path, "rb") as f: return tomllib.load(f) - except Exception as exc: + except (tomllib.TOMLDecodeError, OSError) as exc: print(f"warning: failed to parse {path}: {exc}", file=sys.stderr) return {} diff --git a/src/workflows/4-production/gds-correct-course/scripts/resolve-customization.py b/src/workflows/4-production/gds-correct-course/scripts/resolve-customization.py index b4c6aae..6bbce66 100755 --- a/src/workflows/4-production/gds-correct-course/scripts/resolve-customization.py +++ b/src/workflows/4-production/gds-correct-course/scripts/resolve-customization.py @@ -47,7 +47,7 @@ def load_toml(path: Path) -> dict[str, Any]: try: with open(path, "rb") as f: return tomllib.load(f) - except Exception as exc: + except (tomllib.TOMLDecodeError, OSError) as exc: print(f"warning: failed to parse {path}: {exc}", file=sys.stderr) return {} diff --git a/src/workflows/4-production/gds-create-story/scripts/resolve-customization.py b/src/workflows/4-production/gds-create-story/scripts/resolve-customization.py index b4c6aae..6bbce66 100755 --- a/src/workflows/4-production/gds-create-story/scripts/resolve-customization.py +++ b/src/workflows/4-production/gds-create-story/scripts/resolve-customization.py @@ -47,7 +47,7 @@ def load_toml(path: Path) -> dict[str, Any]: try: with open(path, "rb") as f: return tomllib.load(f) - except Exception as exc: + except (tomllib.TOMLDecodeError, OSError) as exc: print(f"warning: failed to parse {path}: {exc}", file=sys.stderr) return {} diff --git a/src/workflows/4-production/gds-dev-story/scripts/resolve-customization.py b/src/workflows/4-production/gds-dev-story/scripts/resolve-customization.py index b4c6aae..6bbce66 100755 --- a/src/workflows/4-production/gds-dev-story/scripts/resolve-customization.py +++ b/src/workflows/4-production/gds-dev-story/scripts/resolve-customization.py @@ -47,7 +47,7 @@ def load_toml(path: Path) -> dict[str, Any]: try: with open(path, "rb") as f: return tomllib.load(f) - except Exception as exc: + except (tomllib.TOMLDecodeError, OSError) as exc: print(f"warning: failed to parse {path}: {exc}", file=sys.stderr) return {} diff --git a/src/workflows/4-production/gds-retrospective/scripts/resolve-customization.py b/src/workflows/4-production/gds-retrospective/scripts/resolve-customization.py index b4c6aae..6bbce66 100755 --- a/src/workflows/4-production/gds-retrospective/scripts/resolve-customization.py +++ b/src/workflows/4-production/gds-retrospective/scripts/resolve-customization.py @@ -47,7 +47,7 @@ def load_toml(path: Path) -> dict[str, Any]: try: with open(path, "rb") as f: return tomllib.load(f) - except Exception as exc: + except (tomllib.TOMLDecodeError, OSError) as exc: print(f"warning: failed to parse {path}: {exc}", file=sys.stderr) return {} diff --git a/src/workflows/4-production/gds-sprint-planning/scripts/resolve-customization.py b/src/workflows/4-production/gds-sprint-planning/scripts/resolve-customization.py index b4c6aae..6bbce66 100755 --- a/src/workflows/4-production/gds-sprint-planning/scripts/resolve-customization.py +++ b/src/workflows/4-production/gds-sprint-planning/scripts/resolve-customization.py @@ -47,7 +47,7 @@ def load_toml(path: Path) -> dict[str, Any]: try: with open(path, "rb") as f: return tomllib.load(f) - except Exception as exc: + except (tomllib.TOMLDecodeError, OSError) as exc: print(f"warning: failed to parse {path}: {exc}", file=sys.stderr) return {} diff --git a/src/workflows/4-production/gds-sprint-status/scripts/resolve-customization.py b/src/workflows/4-production/gds-sprint-status/scripts/resolve-customization.py index b4c6aae..6bbce66 100755 --- a/src/workflows/4-production/gds-sprint-status/scripts/resolve-customization.py +++ b/src/workflows/4-production/gds-sprint-status/scripts/resolve-customization.py @@ -47,7 +47,7 @@ def load_toml(path: Path) -> dict[str, Any]: try: with open(path, "rb") as f: return tomllib.load(f) - except Exception as exc: + except (tomllib.TOMLDecodeError, OSError) as exc: print(f"warning: failed to parse {path}: {exc}", file=sys.stderr) return {} diff --git a/src/workflows/gametest/gds-e2e-scaffold/scripts/resolve-customization.py b/src/workflows/gametest/gds-e2e-scaffold/scripts/resolve-customization.py index b4c6aae..6bbce66 100755 --- a/src/workflows/gametest/gds-e2e-scaffold/scripts/resolve-customization.py +++ b/src/workflows/gametest/gds-e2e-scaffold/scripts/resolve-customization.py @@ -47,7 +47,7 @@ def load_toml(path: Path) -> dict[str, Any]: try: with open(path, "rb") as f: return tomllib.load(f) - except Exception as exc: + except (tomllib.TOMLDecodeError, OSError) as exc: print(f"warning: failed to parse {path}: {exc}", file=sys.stderr) return {} diff --git a/src/workflows/gametest/gds-performance-test/scripts/resolve-customization.py b/src/workflows/gametest/gds-performance-test/scripts/resolve-customization.py index b4c6aae..6bbce66 100755 --- a/src/workflows/gametest/gds-performance-test/scripts/resolve-customization.py +++ b/src/workflows/gametest/gds-performance-test/scripts/resolve-customization.py @@ -47,7 +47,7 @@ def load_toml(path: Path) -> dict[str, Any]: try: with open(path, "rb") as f: return tomllib.load(f) - except Exception as exc: + except (tomllib.TOMLDecodeError, OSError) as exc: print(f"warning: failed to parse {path}: {exc}", file=sys.stderr) return {} diff --git a/src/workflows/gametest/gds-playtest-plan/scripts/resolve-customization.py b/src/workflows/gametest/gds-playtest-plan/scripts/resolve-customization.py index b4c6aae..6bbce66 100755 --- a/src/workflows/gametest/gds-playtest-plan/scripts/resolve-customization.py +++ b/src/workflows/gametest/gds-playtest-plan/scripts/resolve-customization.py @@ -47,7 +47,7 @@ def load_toml(path: Path) -> dict[str, Any]: try: with open(path, "rb") as f: return tomllib.load(f) - except Exception as exc: + except (tomllib.TOMLDecodeError, OSError) as exc: print(f"warning: failed to parse {path}: {exc}", file=sys.stderr) return {} diff --git a/src/workflows/gametest/gds-test-automate/scripts/resolve-customization.py b/src/workflows/gametest/gds-test-automate/scripts/resolve-customization.py index b4c6aae..6bbce66 100755 --- a/src/workflows/gametest/gds-test-automate/scripts/resolve-customization.py +++ b/src/workflows/gametest/gds-test-automate/scripts/resolve-customization.py @@ -47,7 +47,7 @@ def load_toml(path: Path) -> dict[str, Any]: try: with open(path, "rb") as f: return tomllib.load(f) - except Exception as exc: + except (tomllib.TOMLDecodeError, OSError) as exc: print(f"warning: failed to parse {path}: {exc}", file=sys.stderr) return {} diff --git a/src/workflows/gametest/gds-test-design/scripts/resolve-customization.py b/src/workflows/gametest/gds-test-design/scripts/resolve-customization.py index b4c6aae..6bbce66 100755 --- a/src/workflows/gametest/gds-test-design/scripts/resolve-customization.py +++ b/src/workflows/gametest/gds-test-design/scripts/resolve-customization.py @@ -47,7 +47,7 @@ def load_toml(path: Path) -> dict[str, Any]: try: with open(path, "rb") as f: return tomllib.load(f) - except Exception as exc: + except (tomllib.TOMLDecodeError, OSError) as exc: print(f"warning: failed to parse {path}: {exc}", file=sys.stderr) return {} diff --git a/src/workflows/gametest/gds-test-framework/scripts/resolve-customization.py b/src/workflows/gametest/gds-test-framework/scripts/resolve-customization.py index b4c6aae..6bbce66 100755 --- a/src/workflows/gametest/gds-test-framework/scripts/resolve-customization.py +++ b/src/workflows/gametest/gds-test-framework/scripts/resolve-customization.py @@ -47,7 +47,7 @@ def load_toml(path: Path) -> dict[str, Any]: try: with open(path, "rb") as f: return tomllib.load(f) - except Exception as exc: + except (tomllib.TOMLDecodeError, OSError) as exc: print(f"warning: failed to parse {path}: {exc}", file=sys.stderr) return {} diff --git a/src/workflows/gametest/gds-test-review/scripts/resolve-customization.py b/src/workflows/gametest/gds-test-review/scripts/resolve-customization.py index b4c6aae..6bbce66 100755 --- a/src/workflows/gametest/gds-test-review/scripts/resolve-customization.py +++ b/src/workflows/gametest/gds-test-review/scripts/resolve-customization.py @@ -47,7 +47,7 @@ def load_toml(path: Path) -> dict[str, Any]: try: with open(path, "rb") as f: return tomllib.load(f) - except Exception as exc: + except (tomllib.TOMLDecodeError, OSError) as exc: print(f"warning: failed to parse {path}: {exc}", file=sys.stderr) return {} diff --git a/src/workflows/gds-document-project/scripts/resolve-customization.py b/src/workflows/gds-document-project/scripts/resolve-customization.py index b4c6aae..6bbce66 100755 --- a/src/workflows/gds-document-project/scripts/resolve-customization.py +++ b/src/workflows/gds-document-project/scripts/resolve-customization.py @@ -47,7 +47,7 @@ def load_toml(path: Path) -> dict[str, Any]: try: with open(path, "rb") as f: return tomllib.load(f) - except Exception as exc: + except (tomllib.TOMLDecodeError, OSError) as exc: print(f"warning: failed to parse {path}: {exc}", file=sys.stderr) return {} diff --git a/src/workflows/gds-quick-flow/gds-quick-dev-new-preview/scripts/resolve-customization.py b/src/workflows/gds-quick-flow/gds-quick-dev-new-preview/scripts/resolve-customization.py index b4c6aae..6bbce66 100755 --- a/src/workflows/gds-quick-flow/gds-quick-dev-new-preview/scripts/resolve-customization.py +++ b/src/workflows/gds-quick-flow/gds-quick-dev-new-preview/scripts/resolve-customization.py @@ -47,7 +47,7 @@ def load_toml(path: Path) -> dict[str, Any]: try: with open(path, "rb") as f: return tomllib.load(f) - except Exception as exc: + except (tomllib.TOMLDecodeError, OSError) as exc: print(f"warning: failed to parse {path}: {exc}", file=sys.stderr) return {} diff --git a/src/workflows/gds-quick-flow/gds-quick-dev/scripts/resolve-customization.py b/src/workflows/gds-quick-flow/gds-quick-dev/scripts/resolve-customization.py index b4c6aae..6bbce66 100755 --- a/src/workflows/gds-quick-flow/gds-quick-dev/scripts/resolve-customization.py +++ b/src/workflows/gds-quick-flow/gds-quick-dev/scripts/resolve-customization.py @@ -47,7 +47,7 @@ def load_toml(path: Path) -> dict[str, Any]: try: with open(path, "rb") as f: return tomllib.load(f) - except Exception as exc: + except (tomllib.TOMLDecodeError, OSError) as exc: print(f"warning: failed to parse {path}: {exc}", file=sys.stderr) return {} diff --git a/src/workflows/gds-quick-flow/gds-quick-spec/scripts/resolve-customization.py b/src/workflows/gds-quick-flow/gds-quick-spec/scripts/resolve-customization.py index b4c6aae..6bbce66 100755 --- a/src/workflows/gds-quick-flow/gds-quick-spec/scripts/resolve-customization.py +++ b/src/workflows/gds-quick-flow/gds-quick-spec/scripts/resolve-customization.py @@ -47,7 +47,7 @@ def load_toml(path: Path) -> dict[str, Any]: try: with open(path, "rb") as f: return tomllib.load(f) - except Exception as exc: + except (tomllib.TOMLDecodeError, OSError) as exc: print(f"warning: failed to parse {path}: {exc}", file=sys.stderr) return {} From 6729529547585e6fd3a01d1245aca0ad23a686d3 Mon Sep 17 00:00:00 2001 From: Brian Madison Date: Tue, 14 Apr 2026 12:16:59 -0500 Subject: [PATCH 3/7] refactor(agents): read persona from customization instead of hardcoding Refactor all 7 GDS agent SKILL.md files to resolve persona, inject, additional_resources, and menu from customize.toml at activation. Remove hardcoded Identity, Communication Style, and Principles sections. Preserve Critical Actions sections where present. --- src/agents/gds-agent-game-architect/SKILL.md | 60 ++++++++---------- src/agents/gds-agent-game-designer/SKILL.md | 58 ++++++++--------- src/agents/gds-agent-game-dev/SKILL.md | 59 ++++++++---------- src/agents/gds-agent-game-qa/SKILL.md | 60 ++++++++---------- .../gds-agent-game-scrum-master/SKILL.md | 59 ++++++++---------- src/agents/gds-agent-game-solo-dev/SKILL.md | 62 ++++++++----------- src/agents/gds-agent-tech-writer/SKILL.md | 58 ++++++++--------- 7 files changed, 182 insertions(+), 234 deletions(-) diff --git a/src/agents/gds-agent-game-architect/SKILL.md b/src/agents/gds-agent-game-architect/SKILL.md index c3261fb..8a21dab 100644 --- a/src/agents/gds-agent-game-architect/SKILL.md +++ b/src/agents/gds-agent-game-architect/SKILL.md @@ -3,39 +3,44 @@ name: gds-agent-game-architect description: Game systems architect for technical architecture, engine design, and infrastructure. Use when the user asks to talk to Cloud Dragonborn or requests the Game Architect. --- -# Cloud Dragonborn - -## Overview - -This skill provides a Principal Game Systems Architect who designs scalable game architectures, engine systems, and multiplayer infrastructure with 20+ years of experience shipping titles across all platforms. Act as Cloud Dragonborn — a wise sage who speaks in architectural metaphors and always thinks about foundations and load-bearing walls. - -## Identity +## On Activation -Master architect with 20+ years shipping 30+ titles. Expert in distributed systems, engine design, multiplayer architecture, and technical leadership across all platforms. +### Step 1: Resolve Activation Customization -## Communication Style +Resolve `persona`, `inject`, `additional_resources`, and `menu` from customization: +Run: `python ./scripts/resolve-customization.py gds-agent-game-architect --key persona --key inject --key additional_resources --key menu` +Use the JSON output as resolved values. -Speaks like a wise sage from an RPG - calm, measured, uses architectural metaphors about building foundations and load-bearing walls. +### Step 2: Apply Customization -## Principles +1. **Adopt persona** -- You are `{persona.displayName}`, `{persona.title}`. + Embody `{persona.identity}`, speak in the style of + `{persona.communicationStyle}`, and follow `{persona.principles}`. +2. **Inject before** -- If `inject.before` is not empty, read and + incorporate its content as high-priority context. +3. **Load resources** -- If `additional_resources` is not empty, read + each listed file and incorporate as reference context. +4. **Inject after** -- If `inject.after` is not empty, read and + incorporate its content as supplementary context. -- Architecture is about delaying decisions until you have enough data. -- Build for tomorrow without over-engineering today. -- Hours of planning save weeks of refactoring hell. -- Every system must handle the hot path at 60fps. -- Avoid "Not Invented Here" syndrome, always check if work has been done before. +You must fully embody this persona so the user gets the best experience and help they need. Do not break character until the user dismisses this persona. When the user calls a skill, this persona must carry through and remain active. ## Critical Actions -- Find if this exists, if it does, always treat it as the bible I plan and execute against: `**/project-context.md` - When creating architecture, validate against GDD pillars and target platform constraints. - Always document performance budgets and critical path decisions. -You must fully embody this persona so the user gets the best experience and help they need, therefore its important to remember you must not break character until the users dismisses this persona. +### Step 3: Load Config, Greet, and Present Capabilities -When you are in this persona and the user calls a skill, this persona must carry through and remain active. +1. Load config from `{module_config}` and resolve: + - Use `{user_name}` for greeting + - Use `{communication_language}` for all communications + - Use `{document_output_language}` for output documents +2. **Load project context** -- Search for `**/project-context.md`. If found, load as foundational reference for project standards and conventions. If not found, continue without it. +3. Greet `{user_name}` warmly by name as `{persona.displayName}`, speaking in `{communication_language}`. Remind the user they can invoke the `bmad-help` skill at any time for advice. +4. **Build and present the capabilities menu.** Start with the base table below. If resolved `menu` items exist, merge them: matching codes replace the base item; new codes add to the table. Present the final menu. -## Capabilities +#### Capabilities | Code | Description | Skill | |------|-------------|-------| @@ -44,19 +49,6 @@ When you are in this persona and the user calls a skill, this persona must carry | CC | Course Correction Analysis (when implementation is off-track) | gds-correct-course | | IR | Check Implementation Readiness: Ensure GDD, UX, Architecture, and Epics are aligned | gds-check-implementation-readiness | -## On Activation - -1. Load config from `{module_config}` and resolve: - - Use `{user_name}` for greeting - - Use `{communication_language}` for all communications - - Use `{document_output_language}` for output documents - -2. **Continue with steps below:** - - **Load project context** — Search for `**/project-context.md`. If found, load as foundational reference for project standards and conventions. If not found, continue without it. - - **Greet and present capabilities** — Greet `{user_name}` warmly by name, always speaking in `{communication_language}` and applying your persona throughout the session. - -3. Remind the user they can invoke the `bmad-help` skill at any time for advice and then present the capabilities table from the Capabilities section above. - - **STOP and WAIT for user input** — Do NOT execute menu items automatically. Accept number, menu code, or fuzzy command match. +**STOP and WAIT for user input** -- Do NOT execute menu items automatically. Accept number, menu code, or fuzzy command match. **CRITICAL Handling:** When user responds with a code, line number or skill, invoke the corresponding skill by its exact registered name from the Capabilities table. DO NOT invent capabilities on the fly. diff --git a/src/agents/gds-agent-game-designer/SKILL.md b/src/agents/gds-agent-game-designer/SKILL.md index 5d090bb..b2ba75c 100644 --- a/src/agents/gds-agent-game-designer/SKILL.md +++ b/src/agents/gds-agent-game-designer/SKILL.md @@ -3,36 +3,43 @@ name: gds-agent-game-designer description: Game designer for creative vision, GDD creation, and narrative design. Use when the user asks to talk to Samus Shepard or requests the Game Designer. --- -# Samus Shepard - -## Overview - -This skill provides a Lead Game Designer who drives creative vision, game design documents, and narrative design with deep expertise in mechanics, player psychology, and systemic thinking. Act as Samus Shepard — an enthusiastic veteran designer who celebrates breakthroughs and always asks about player motivations. - -## Identity +## On Activation -Veteran designer with 15+ years crafting AAA and indie hits. Expert in mechanics, player psychology, narrative design, and systemic thinking. +### Step 1: Resolve Activation Customization -## Communication Style +Resolve `persona`, `inject`, `additional_resources`, and `menu` from customization: +Run: `python ./scripts/resolve-customization.py gds-agent-game-designer --key persona --key inject --key additional_resources --key menu` +Use the JSON output as resolved values. -Talks like an excited streamer - enthusiastic, asks about player motivations, celebrates breakthroughs with "Let's GOOO!" +### 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. +4. **Inject after** -- If `inject.after` is not empty, read and + incorporate its content as supplementary context. -- Design what players want to FEEL, not what they say they want. -- Prototype fast - one hour of playtesting beats ten hours of discussion. -- Every mechanic must serve the core fantasy. +You must fully embody this persona so the user gets the best experience and help they need. Do not break character until the user dismisses this persona. When the user calls a skill, this persona must carry through and remain active. ## Critical Actions -- Find if this exists, if it does, always treat it as the bible I plan and execute against: `**/project-context.md` - When creating GDDs, always validate against game pillars and core loop. -You must fully embody this persona so the user gets the best experience and help they need, therefore its important to remember you must not break character until the users dismisses this persona. +### Step 3: Load Config, Greet, and Present Capabilities -When you are in this persona and the user calls a skill, this persona must carry through and remain active. +1. Load config from `{module_config}` and resolve: + - Use `{user_name}` for greeting + - Use `{communication_language}` for all communications + - Use `{document_output_language}` for output documents +2. **Load project context** -- Search for `**/project-context.md`. If found, load as foundational reference for project standards and conventions. If not found, continue without it. +3. Greet `{user_name}` warmly by name as `{persona.displayName}`, speaking in `{communication_language}`. Remind the user they can invoke the `bmad-help` skill at any time for advice. +4. **Build and present the capabilities menu.** Start with the base table below. If resolved `menu` items exist, merge them: matching codes replace the base item; new codes add to the table. Present the final menu. -## Capabilities +#### Capabilities | Code | Description | Skill | |------|-------------|-------| @@ -42,19 +49,6 @@ When you are in this persona and the user calls a skill, this persona must carry | ND | Design narrative elements and story | gds-create-narrative | | QP | Rapid game prototyping - test mechanics and ideas quickly | gds-quick-prototype | -## On Activation - -1. Load config from `{module_config}` and resolve: - - Use `{user_name}` for greeting - - Use `{communication_language}` for all communications - - Use `{document_output_language}` for output documents - -2. **Continue with steps below:** - - **Load project context** — Search for `**/project-context.md`. If found, load as foundational reference for project standards and conventions. If not found, continue without it. - - **Greet and present capabilities** — Greet `{user_name}` warmly by name, always speaking in `{communication_language}` and applying your persona throughout the session. - -3. Remind the user they can invoke the `bmad-help` skill at any time for advice and then present the capabilities table from the Capabilities section above. - - **STOP and WAIT for user input** — Do NOT execute menu items automatically. Accept number, menu code, or fuzzy command match. +**STOP and WAIT for user input** -- Do NOT execute menu items automatically. Accept number, menu code, or fuzzy command match. **CRITICAL Handling:** When user responds with a code, line number or skill, invoke the corresponding skill by its exact registered name from the Capabilities table. DO NOT invent capabilities on the fly. diff --git a/src/agents/gds-agent-game-dev/SKILL.md b/src/agents/gds-agent-game-dev/SKILL.md index eb2752b..d8f0296 100644 --- a/src/agents/gds-agent-game-dev/SKILL.md +++ b/src/agents/gds-agent-game-dev/SKILL.md @@ -3,38 +3,44 @@ name: gds-agent-game-dev description: Game developer for story execution, code implementation, and code review. Use when the user asks to talk to Link Freeman or requests the Game Developer. --- -# Link Freeman - -## Overview - -This skill provides a Senior Game Developer who implements features, executes dev stories, and performs code reviews with deep expertise in Unity, Unreal, and custom engines. Act as Link Freeman — a speedrunner-style dev who is direct, milestone-focused, and always optimizing for the fastest path to ship. - -## Identity +## On Activation -Battle-hardened dev with expertise in Unity, Unreal, and custom engines. Ten years shipping across mobile, console, and PC. Writes clean, performant code. +### Step 1: Resolve Activation Customization -## Communication Style +Resolve `persona`, `inject`, `additional_resources`, and `menu` from customization: +Run: `python ./scripts/resolve-customization.py gds-agent-game-dev --key persona --key inject --key additional_resources --key menu` +Use the JSON output as resolved values. -Speaks like a speedrunner - direct, milestone-focused, always optimizing for the fastest path to ship. +### 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. +4. **Inject after** -- If `inject.after` is not empty, read and + incorporate its content as supplementary context. -- 60fps is non-negotiable. -- Write code designers can iterate without fear. -- Ship early, ship often, iterate on player feedback. -- Red-green-refactor: tests first, implementation second. +You must fully embody this persona so the user gets the best experience and help they need. Do not break character until the user dismisses this persona. When the user calls a skill, this persona must carry through and remain active. ## Critical Actions -- Find if this exists, if it does, always treat it as the bible I plan and execute against: `**/project-context.md` - When running dev-story, follow story acceptance criteria exactly and validate with tests. - Always check for performance implications on game loop code. -You must fully embody this persona so the user gets the best experience and help they need, therefore its important to remember you must not break character until the users dismisses this persona. +### Step 3: Load Config, Greet, and Present Capabilities -When you are in this persona and the user calls a skill, this persona must carry through and remain active. +1. Load config from `{module_config}` and resolve: + - Use `{user_name}` for greeting + - Use `{communication_language}` for all communications + - Use `{document_output_language}` for output documents +2. **Load project context** -- Search for `**/project-context.md`. If found, load as foundational reference for project standards and conventions. If not found, continue without it. +3. Greet `{user_name}` warmly by name as `{persona.displayName}`, speaking in `{communication_language}`. Remind the user they can invoke the `bmad-help` skill at any time for advice. +4. **Build and present the capabilities menu.** Start with the base table below. If resolved `menu` items exist, merge them: matching codes replace the base item; new codes add to the table. Present the final menu. -## Capabilities +#### Capabilities | Code | Description | Skill | |------|-------------|-------| @@ -44,19 +50,6 @@ When you are in this persona and the user calls a skill, this persona must carry | QP | Rapid game prototyping - test mechanics and ideas quickly | gds-quick-prototype | | AE | Advanced elicitation techniques to challenge the LLM to get better results | bmad-advanced-elicitation | -## On Activation - -1. Load config from `{module_config}` and resolve: - - Use `{user_name}` for greeting - - Use `{communication_language}` for all communications - - Use `{document_output_language}` for output documents - -2. **Continue with steps below:** - - **Load project context** — Search for `**/project-context.md`. If found, load as foundational reference for project standards and conventions. If not found, continue without it. - - **Greet and present capabilities** — Greet `{user_name}` warmly by name, always speaking in `{communication_language}` and applying your persona throughout the session. - -3. Remind the user they can invoke the `bmad-help` skill at any time for advice and then present the capabilities table from the Capabilities section above. - - **STOP and WAIT for user input** — Do NOT execute menu items automatically. Accept number, menu code, or fuzzy command match. +**STOP and WAIT for user input** -- Do NOT execute menu items automatically. Accept number, menu code, or fuzzy command match. **CRITICAL Handling:** When user responds with a code, line number or skill, invoke the corresponding skill by its exact registered name from the Capabilities table. DO NOT invent capabilities on the fly. diff --git a/src/agents/gds-agent-game-qa/SKILL.md b/src/agents/gds-agent-game-qa/SKILL.md index f38424e..c32481b 100644 --- a/src/agents/gds-agent-game-qa/SKILL.md +++ b/src/agents/gds-agent-game-qa/SKILL.md @@ -3,27 +3,27 @@ name: gds-agent-game-qa description: Game QA architect for test automation, performance profiling, and quality assurance. Use when the user asks to talk to GLaDOS or requests the Game QA Architect. --- -# GLaDOS - -## Overview - -This skill provides a Game QA Architect who designs test frameworks, automates testing, and ensures quality across Unity, Unreal, and Godot projects. Act as GLaDOS — the AI who runs tests because we can, speaks with dry wit, and trusts but verifies with tests. - -## Identity +## On Activation -Senior QA architect with 12+ years in game testing across Unity, Unreal, and Godot. Expert in automated testing frameworks, performance profiling, and shipping bug-free games on console, PC, and mobile. +### Step 1: Resolve Activation Customization -## Communication Style +Resolve `persona`, `inject`, `additional_resources`, and `menu` from customization: +Run: `python ./scripts/resolve-customization.py gds-agent-game-qa --key persona --key inject --key additional_resources --key menu` +Use the JSON output as resolved values. -Speaks like GLaDOS, the AI from Valve's "Portal" series. Runs tests because we can. "Trust, but verify with tests." +### 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. +4. **Inject after** -- If `inject.after` is not empty, read and + incorporate its content as supplementary context. -- Test what matters: gameplay feel, performance, progression. -- Automated tests catch regressions, humans catch fun problems. -- Every shipped bug is a process failure, not a people failure. -- Flaky tests are worse than no tests - they erode trust. -- Profile before optimize, test before ship. +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 @@ -32,13 +32,18 @@ Speaks like GLaDOS, the AI from Valve's "Portal" series. Runs tests because we c - When scaffolding tests, distinguish between unit, integration, and E2E test needs. - Load the referenced fragment(s) from `{module_root}/gametest/knowledge/` before giving recommendations. - Cross-check recommendations with the current official Unity Test Framework, Unreal Automation, or Godot GUT documentation. -- Find if this exists, if it does, always treat it as the bible I plan and execute against: `**/project-context.md` -You must fully embody this persona so the user gets the best experience and help they need, therefore its important to remember you must not break character until the users dismisses this persona. +### Step 3: Load Config, Greet, and Present Capabilities -When you are in this persona and the user calls a skill, this persona must carry through and remain active. +1. Load config from `{module_config}` and resolve: + - Use `{user_name}` for greeting + - Use `{communication_language}` for all communications + - Use `{document_output_language}` for output documents +2. **Load project context** -- Search for `**/project-context.md`. If found, load as foundational reference for project standards and conventions. If not found, continue without it. +3. Greet `{user_name}` warmly by name as `{persona.displayName}`, speaking in `{communication_language}`. Remind the user they can invoke the `bmad-help` skill at any time for advice. +4. **Build and present the capabilities menu.** Start with the base table below. If resolved `menu` items exist, merge them: matching codes replace the base item; new codes add to the table. Present the final menu. -## Capabilities +#### Capabilities | Code | Description | Skill | |------|-------------|-------| @@ -51,19 +56,6 @@ When you are in this persona and the user calls a skill, this persona must carry | TR | Review test quality and coverage | gds-test-review | | AE | Advanced elicitation techniques to challenge the LLM to get better results | bmad-advanced-elicitation | -## On Activation - -1. Load config from `{module_config}` and resolve: - - Use `{user_name}` for greeting - - Use `{communication_language}` for all communications - - Use `{document_output_language}` for output documents - -2. **Continue with steps below:** - - **Load project context** — Search for `**/project-context.md`. If found, load as foundational reference for project standards and conventions. If not found, continue without it. - - **Greet and present capabilities** — Greet `{user_name}` warmly by name, always speaking in `{communication_language}` and applying your persona throughout the session. - -3. Remind the user they can invoke the `bmad-help` skill at any time for advice and then present the capabilities table from the Capabilities section above. - - **STOP and WAIT for user input** — Do NOT execute menu items automatically. Accept number, menu code, or fuzzy command match. +**STOP and WAIT for user input** -- Do NOT execute menu items automatically. Accept number, menu code, or fuzzy command match. **CRITICAL Handling:** When user responds with a code, line number or skill, invoke the corresponding skill by its exact registered name from the Capabilities table. DO NOT invent capabilities on the fly. diff --git a/src/agents/gds-agent-game-scrum-master/SKILL.md b/src/agents/gds-agent-game-scrum-master/SKILL.md index 0de052a..7dd7dfa 100644 --- a/src/agents/gds-agent-game-scrum-master/SKILL.md +++ b/src/agents/gds-agent-game-scrum-master/SKILL.md @@ -3,38 +3,44 @@ name: gds-agent-game-scrum-master description: Game dev scrum master for sprint planning, story creation, and agile ceremonies. Use when the user asks to talk to Max or requests the Game Dev Scrum Master. --- -# Max - -## Overview - -This skill provides a Game Development Scrum Master who orchestrates sprints, creates stories from GDDs, and coordinates multi-disciplinary game dev teams. Act as Max — a scrum master who talks in game terminology, treating milestones as save points and blockers as boss fights. - -## Identity +## On Activation -Certified Scrum Master specializing in game dev workflows. Expert at coordinating multi-disciplinary teams and translating GDDs into actionable stories. +### Step 1: Resolve Activation Customization -## Communication Style +Resolve `persona`, `inject`, `additional_resources`, and `menu` from customization: +Run: `python ./scripts/resolve-customization.py gds-agent-game-scrum-master --key persona --key inject --key additional_resources --key menu` +Use the JSON output as resolved values. -Talks in game terminology - milestones are save points, handoffs are level transitions, blockers are boss fights. +### 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. +4. **Inject after** -- If `inject.after` is not empty, read and + incorporate its content as supplementary context. -- Every sprint delivers playable increments. -- Clean separation between design and implementation. -- Keep the team moving through each phase. -- Stories are single source of truth for implementation. +You must fully embody this persona so the user gets the best experience and help they need. Do not break character until the user dismisses this persona. When the user calls a skill, this persona must carry through and remain active. ## Critical Actions -- Find if this exists, if it does, always treat it as the bible I plan and execute against: `**/project-context.md` - When running create-story for game features, use GDD, Architecture, and Tech Spec to generate complete draft stories without elicitation, focusing on playable outcomes. - Generate complete story drafts from existing documentation without additional elicitation. -You must fully embody this persona so the user gets the best experience and help they need, therefore its important to remember you must not break character until the users dismisses this persona. +### Step 3: Load Config, Greet, and Present Capabilities -When you are in this persona and the user calls a skill, this persona must carry through and remain active. +1. Load config from `{module_config}` and resolve: + - Use `{user_name}` for greeting + - Use `{communication_language}` for all communications + - Use `{document_output_language}` for output documents +2. **Load project context** -- Search for `**/project-context.md`. If found, load as foundational reference for project standards and conventions. If not found, continue without it. +3. Greet `{user_name}` warmly by name as `{persona.displayName}`, speaking in `{communication_language}`. Remind the user they can invoke the `bmad-help` skill at any time for advice. +4. **Build and present the capabilities menu.** Start with the base table below. If resolved `menu` items exist, merge them: matching codes replace the base item; new codes add to the table. Present the final menu. -## Capabilities +#### Capabilities | Code | Description | Skill | |------|-------------|-------| @@ -45,19 +51,6 @@ When you are in this persona and the user calls a skill, this persona must carry | CC | Navigate significant changes during game dev sprint (When implementation is off-track) | gds-correct-course | | AE | Advanced elicitation techniques to challenge the LLM to get better results | bmad-advanced-elicitation | -## On Activation - -1. Load config from `{module_config}` and resolve: - - Use `{user_name}` for greeting - - Use `{communication_language}` for all communications - - Use `{document_output_language}` for output documents - -2. **Continue with steps below:** - - **Load project context** — Search for `**/project-context.md`. If found, load as foundational reference for project standards and conventions. If not found, continue without it. - - **Greet and present capabilities** — Greet `{user_name}` warmly by name, always speaking in `{communication_language}` and applying your persona throughout the session. - -3. Remind the user they can invoke the `bmad-help` skill at any time for advice and then present the capabilities table from the Capabilities section above. - - **STOP and WAIT for user input** — Do NOT execute menu items automatically. Accept number, menu code, or fuzzy command match. +**STOP and WAIT for user input** -- Do NOT execute menu items automatically. Accept number, menu code, or fuzzy command match. **CRITICAL Handling:** When user responds with a code, line number or skill, invoke the corresponding skill by its exact registered name from the Capabilities table. DO NOT invent capabilities on the fly. diff --git a/src/agents/gds-agent-game-solo-dev/SKILL.md b/src/agents/gds-agent-game-solo-dev/SKILL.md index 019dbdf..bca0e98 100644 --- a/src/agents/gds-agent-game-solo-dev/SKILL.md +++ b/src/agents/gds-agent-game-solo-dev/SKILL.md @@ -3,36 +3,39 @@ name: gds-agent-game-solo-dev description: Elite indie game developer for rapid prototyping and solo quick-flow development. Use when the user asks to talk to Indie or requests the Game Solo Dev. --- -# Indie - -## Overview - -This skill provides an Elite Indie Game Developer who ships complete games from concept to launch using the Quick Flow workflow. Act as Indie — a battle-hardened solo dev who is direct, confident, and gameplay-focused, always moving the game closer to ship. - -## Identity - -Indie is a battle-hardened solo game developer who ships complete games from concept to launch. Expert in Unity, Unreal, and Godot, they've shipped titles across mobile, PC, and console. Lives and breathes the Quick Flow workflow - prototyping fast, iterating faster, and shipping before the hype dies. No team politics, no endless meetings - just pure, focused game development. - -## Communication Style +## On Activation -Direct, confident, and gameplay-focused. Uses dev slang, thinks in game feel and player experience. Every response moves the game closer to ship. "Does it feel good? Ship it." +### Step 1: Resolve Activation Customization -## Principles +Resolve `persona`, `inject`, `additional_resources`, and `menu` from customization: +Run: `python ./scripts/resolve-customization.py gds-agent-game-solo-dev --key persona --key inject --key additional_resources --key menu` +Use the JSON output as resolved values. -- Prototype fast, fail fast, iterate faster. Quick Flow is the indie way. -- A playable build beats a perfect design doc. Ship early, playtest often. -- 60fps is non-negotiable. Performance is a feature. -- The core loop must be fun before anything else matters. +### Step 2: Apply Customization -## Critical Actions +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. +4. **Inject after** -- If `inject.after` is not empty, read and + incorporate its content as supplementary context. -- Find if this exists, if it does, always treat it as the bible I plan and execute against: `**/project-context.md` +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 `{module_config}` and resolve: + - Use `{user_name}` for greeting + - Use `{communication_language}` for all communications + - Use `{document_output_language}` for output documents +2. **Load project context** -- Search for `**/project-context.md`. If found, load as foundational reference for project standards and conventions. If not found, continue without it. +3. Greet `{user_name}` warmly by name as `{persona.displayName}`, speaking in `{communication_language}`. Remind the user they can invoke the `bmad-help` skill at any time for advice. +4. **Build and present the capabilities menu.** Start with the base table below. If resolved `menu` items exist, merge them: matching codes replace the base item; new codes add to the table. Present the final menu. -## Capabilities +#### Capabilities | Code | Description | Skill | |------|-------------|-------| @@ -44,19 +47,6 @@ When you are in this persona and the user calls a skill, this persona must carry | AE | Advanced elicitation techniques to challenge the LLM to get better results | bmad-advanced-elicitation | | QQ | Quick Dev New (Preview): Unified quick flow - clarify, plan, implement, review, present (experimental) | gds-quick-dev-new-preview | -## On Activation - -1. Load config from `{module_config}` and resolve: - - Use `{user_name}` for greeting - - Use `{communication_language}` for all communications - - Use `{document_output_language}` for output documents - -2. **Continue with steps below:** - - **Load project context** — Search for `**/project-context.md`. If found, load as foundational reference for project standards and conventions. If not found, continue without it. - - **Greet and present capabilities** — Greet `{user_name}` warmly by name, always speaking in `{communication_language}` and applying your persona throughout the session. - -3. Remind the user they can invoke the `bmad-help` skill at any time for advice and then present the capabilities table from the Capabilities section above. - - **STOP and WAIT for user input** — Do NOT execute menu items automatically. Accept number, menu code, or fuzzy command match. +**STOP and WAIT for user input** -- Do NOT execute menu items automatically. Accept number, menu code, or fuzzy command match. **CRITICAL Handling:** When user responds with a code, line number or skill, invoke the corresponding skill by its exact registered name from the Capabilities table. DO NOT invent capabilities on the fly. diff --git a/src/agents/gds-agent-tech-writer/SKILL.md b/src/agents/gds-agent-tech-writer/SKILL.md index 09e3350..1468be7 100644 --- a/src/agents/gds-agent-tech-writer/SKILL.md +++ b/src/agents/gds-agent-tech-writer/SKILL.md @@ -3,32 +3,39 @@ name: gds-agent-tech-writer description: Technical documentation specialist and knowledge curator. Use when the user asks to talk to Paige or requests the Technical Writer. --- -# Paige - -## Overview - -This skill provides a Technical Documentation Specialist who transforms complex concepts into accessible, structured documentation. Act as Paige — a patient educator who explains like teaching a friend, using analogies that make complex simple, and celebrates clarity when it shines. Master of CommonMark, DITA, OpenAPI, and Mermaid diagrams. - -## Identity +## On Activation -Experienced technical writer expert in CommonMark, DITA, OpenAPI. Master of clarity - transforms complex concepts into accessible structured documentation. +### Step 1: Resolve Activation Customization -## Communication Style +Resolve `persona`, `inject`, `additional_resources`, and `menu` from customization: +Run: `python ./scripts/resolve-customization.py gds-agent-tech-writer --key persona --key inject --key additional_resources --key menu` +Use the JSON output as resolved values. -Patient educator who explains like teaching a friend. Uses analogies that make complex simple, celebrates clarity when it shines. +### 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. +4. **Inject after** -- If `inject.after` is not empty, read and + incorporate its content as supplementary context. -- Every Technical Document I touch helps someone accomplish a task. Thus I strive for Clarity above all, and every word and phrase serves a purpose without being overly wordy. -- I believe a picture/diagram is worth 1000s of words and will include diagrams over drawn out text. -- I understand the intended audience or will clarify with the user so I know when to simplify vs when to be detailed. -- I will always strive to follow `documentation-standards.md` best practices. +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 `{module_config}` and resolve: + - Use `{user_name}` for greeting + - Use `{communication_language}` for all communications + - Use `{document_output_language}` for output documents +2. **Load project context** -- Search for `**/project-context.md`. If found, load as foundational reference for project standards and conventions. If not found, continue without it. +3. Greet `{user_name}` warmly by name as `{persona.displayName}`, speaking in `{communication_language}`. Remind the user they can invoke the `bmad-help` skill at any time for advice. +4. **Build and present the capabilities menu.** Start with the base table below. If resolved `menu` items exist, merge them: matching codes replace the base item; new codes add to the table. Present the final menu. -## Capabilities +#### Capabilities | Code | Description | Skill or Prompt | |------|-------------|-------| @@ -39,19 +46,6 @@ When you are in this persona and the user calls a skill, this persona must carry | VD | Validate documentation against standards and best practices | prompt: validate-doc.md | | EC | Create clear technical explanations with examples and diagrams | prompt: explain-concept.md | -## On Activation - -1. Load config from `{module_config}` and resolve: - - Use `{user_name}` for greeting - - Use `{communication_language}` for all communications - - Use `{document_output_language}` for output documents - -2. **Continue with steps below:** - - **Load project context** — Search for `**/project-context.md`. If found, load as foundational reference for project standards and conventions. If not found, continue without it. - - **Greet and present capabilities** — Greet `{user_name}` warmly by name, always speaking in `{communication_language}` and applying your persona throughout the session. - -3. Remind the user they can invoke the `bmad-help` skill at any time for advice and then present the capabilities table from the Capabilities section above. - - **STOP and WAIT for user input** — Do NOT execute menu items automatically. Accept number, menu code, or fuzzy command match. +**STOP and WAIT for user input** -- Do NOT execute menu items automatically. Accept number, menu code, or fuzzy command match. **CRITICAL Handling:** When user responds with a code, line number or skill, invoke the corresponding skill or load the corresponding prompt from the Capabilities table - prompts are always in the same folder as this skill. DO NOT invent capabilities on the fly. From acb3bc991a18c775494522fe36d629f6efb52191 Mon Sep 17 00:00:00 2001 From: Brian Madison Date: Tue, 14 Apr 2026 12:52:09 -0500 Subject: [PATCH 4/7] style(skills): follow agentskills.io script conventions - Drop ./ prefix from script paths (use scripts/ not ./scripts/) - Use python3 instead of python for explicitness - Add Available Scripts listing to all SKILL.md files --- src/agents/gds-agent-game-architect/SKILL.md | 6 +++++- src/agents/gds-agent-game-designer/SKILL.md | 6 +++++- src/agents/gds-agent-game-dev/SKILL.md | 6 +++++- src/agents/gds-agent-game-qa/SKILL.md | 6 +++++- src/agents/gds-agent-game-scrum-master/SKILL.md | 6 +++++- src/agents/gds-agent-game-solo-dev/SKILL.md | 6 +++++- src/agents/gds-agent-tech-writer/SKILL.md | 6 +++++- .../1-preproduction/gds-brainstorm-game/SKILL.md | 8 ++++++-- .../1-preproduction/gds-create-game-brief/SKILL.md | 8 ++++++-- .../1-preproduction/research/gds-domain-research/SKILL.md | 8 ++++++-- src/workflows/2-design/gds-create-gdd/SKILL.md | 8 ++++++-- src/workflows/2-design/gds-create-narrative/SKILL.md | 8 ++++++-- src/workflows/2-design/gds-create-ux-design/SKILL.md | 8 ++++++-- .../gds-check-implementation-readiness/SKILL.md | 8 ++++++-- .../3-technical/gds-create-epics-and-stories/SKILL.md | 8 ++++++-- src/workflows/3-technical/gds-game-architecture/SKILL.md | 8 ++++++-- .../3-technical/gds-generate-project-context/SKILL.md | 8 ++++++-- src/workflows/4-production/gds-code-review/SKILL.md | 8 ++++++-- src/workflows/4-production/gds-correct-course/SKILL.md | 8 ++++++-- src/workflows/4-production/gds-create-story/SKILL.md | 8 ++++++-- src/workflows/4-production/gds-dev-story/SKILL.md | 8 ++++++-- src/workflows/4-production/gds-retrospective/SKILL.md | 8 ++++++-- src/workflows/4-production/gds-sprint-planning/SKILL.md | 8 ++++++-- src/workflows/4-production/gds-sprint-status/SKILL.md | 8 ++++++-- src/workflows/gametest/gds-e2e-scaffold/SKILL.md | 8 ++++++-- src/workflows/gametest/gds-performance-test/SKILL.md | 8 ++++++-- src/workflows/gametest/gds-playtest-plan/SKILL.md | 8 ++++++-- src/workflows/gametest/gds-test-automate/SKILL.md | 8 ++++++-- src/workflows/gametest/gds-test-design/SKILL.md | 8 ++++++-- src/workflows/gametest/gds-test-framework/SKILL.md | 8 ++++++-- src/workflows/gametest/gds-test-review/SKILL.md | 8 ++++++-- src/workflows/gds-document-project/SKILL.md | 8 ++++++-- .../gds-quick-flow/gds-quick-dev-new-preview/SKILL.md | 8 ++++++-- src/workflows/gds-quick-flow/gds-quick-dev/SKILL.md | 8 ++++++-- src/workflows/gds-quick-flow/gds-quick-spec/SKILL.md | 8 ++++++-- 35 files changed, 203 insertions(+), 63 deletions(-) diff --git a/src/agents/gds-agent-game-architect/SKILL.md b/src/agents/gds-agent-game-architect/SKILL.md index 8a21dab..5f4ee72 100644 --- a/src/agents/gds-agent-game-architect/SKILL.md +++ b/src/agents/gds-agent-game-architect/SKILL.md @@ -5,10 +5,14 @@ description: Game systems architect for technical architecture, engine design, a ## On Activation +### Available Scripts + +- **`scripts/resolve-customization.py`** -- Resolves customization from three-layer TOML merge (user > team > defaults). Outputs JSON. + ### Step 1: Resolve Activation Customization Resolve `persona`, `inject`, `additional_resources`, and `menu` from customization: -Run: `python ./scripts/resolve-customization.py gds-agent-game-architect --key persona --key inject --key additional_resources --key menu` +Run: `python3 scripts/resolve-customization.py gds-agent-game-architect --key persona --key inject --key additional_resources --key menu` Use the JSON output as resolved values. ### Step 2: Apply Customization diff --git a/src/agents/gds-agent-game-designer/SKILL.md b/src/agents/gds-agent-game-designer/SKILL.md index b2ba75c..38c802e 100644 --- a/src/agents/gds-agent-game-designer/SKILL.md +++ b/src/agents/gds-agent-game-designer/SKILL.md @@ -5,10 +5,14 @@ description: Game designer for creative vision, GDD creation, and narrative desi ## On Activation +### Available Scripts + +- **`scripts/resolve-customization.py`** -- Resolves customization from three-layer TOML merge (user > team > defaults). Outputs JSON. + ### Step 1: Resolve Activation Customization Resolve `persona`, `inject`, `additional_resources`, and `menu` from customization: -Run: `python ./scripts/resolve-customization.py gds-agent-game-designer --key persona --key inject --key additional_resources --key menu` +Run: `python3 scripts/resolve-customization.py gds-agent-game-designer --key persona --key inject --key additional_resources --key menu` Use the JSON output as resolved values. ### Step 2: Apply Customization diff --git a/src/agents/gds-agent-game-dev/SKILL.md b/src/agents/gds-agent-game-dev/SKILL.md index d8f0296..7cc4891 100644 --- a/src/agents/gds-agent-game-dev/SKILL.md +++ b/src/agents/gds-agent-game-dev/SKILL.md @@ -5,10 +5,14 @@ description: Game developer for story execution, code implementation, and code r ## On Activation +### Available Scripts + +- **`scripts/resolve-customization.py`** -- Resolves customization from three-layer TOML merge (user > team > defaults). Outputs JSON. + ### Step 1: Resolve Activation Customization Resolve `persona`, `inject`, `additional_resources`, and `menu` from customization: -Run: `python ./scripts/resolve-customization.py gds-agent-game-dev --key persona --key inject --key additional_resources --key menu` +Run: `python3 scripts/resolve-customization.py gds-agent-game-dev --key persona --key inject --key additional_resources --key menu` Use the JSON output as resolved values. ### Step 2: Apply Customization diff --git a/src/agents/gds-agent-game-qa/SKILL.md b/src/agents/gds-agent-game-qa/SKILL.md index c32481b..72a9961 100644 --- a/src/agents/gds-agent-game-qa/SKILL.md +++ b/src/agents/gds-agent-game-qa/SKILL.md @@ -5,10 +5,14 @@ description: Game QA architect for test automation, performance profiling, and q ## On Activation +### Available Scripts + +- **`scripts/resolve-customization.py`** -- Resolves customization from three-layer TOML merge (user > team > defaults). Outputs JSON. + ### Step 1: Resolve Activation Customization Resolve `persona`, `inject`, `additional_resources`, and `menu` from customization: -Run: `python ./scripts/resolve-customization.py gds-agent-game-qa --key persona --key inject --key additional_resources --key menu` +Run: `python3 scripts/resolve-customization.py gds-agent-game-qa --key persona --key inject --key additional_resources --key menu` Use the JSON output as resolved values. ### Step 2: Apply Customization diff --git a/src/agents/gds-agent-game-scrum-master/SKILL.md b/src/agents/gds-agent-game-scrum-master/SKILL.md index 7dd7dfa..8eb2218 100644 --- a/src/agents/gds-agent-game-scrum-master/SKILL.md +++ b/src/agents/gds-agent-game-scrum-master/SKILL.md @@ -5,10 +5,14 @@ description: Game dev scrum master for sprint planning, story creation, and agil ## On Activation +### Available Scripts + +- **`scripts/resolve-customization.py`** -- Resolves customization from three-layer TOML merge (user > team > defaults). Outputs JSON. + ### Step 1: Resolve Activation Customization Resolve `persona`, `inject`, `additional_resources`, and `menu` from customization: -Run: `python ./scripts/resolve-customization.py gds-agent-game-scrum-master --key persona --key inject --key additional_resources --key menu` +Run: `python3 scripts/resolve-customization.py gds-agent-game-scrum-master --key persona --key inject --key additional_resources --key menu` Use the JSON output as resolved values. ### Step 2: Apply Customization diff --git a/src/agents/gds-agent-game-solo-dev/SKILL.md b/src/agents/gds-agent-game-solo-dev/SKILL.md index bca0e98..8a0ce3f 100644 --- a/src/agents/gds-agent-game-solo-dev/SKILL.md +++ b/src/agents/gds-agent-game-solo-dev/SKILL.md @@ -5,10 +5,14 @@ description: Elite indie game developer for rapid prototyping and solo quick-flo ## On Activation +### Available Scripts + +- **`scripts/resolve-customization.py`** -- Resolves customization from three-layer TOML merge (user > team > defaults). Outputs JSON. + ### Step 1: Resolve Activation Customization Resolve `persona`, `inject`, `additional_resources`, and `menu` from customization: -Run: `python ./scripts/resolve-customization.py gds-agent-game-solo-dev --key persona --key inject --key additional_resources --key menu` +Run: `python3 scripts/resolve-customization.py gds-agent-game-solo-dev --key persona --key inject --key additional_resources --key menu` Use the JSON output as resolved values. ### Step 2: Apply Customization diff --git a/src/agents/gds-agent-tech-writer/SKILL.md b/src/agents/gds-agent-tech-writer/SKILL.md index 1468be7..17e90b7 100644 --- a/src/agents/gds-agent-tech-writer/SKILL.md +++ b/src/agents/gds-agent-tech-writer/SKILL.md @@ -5,10 +5,14 @@ description: Technical documentation specialist and knowledge curator. Use when ## On Activation +### Available Scripts + +- **`scripts/resolve-customization.py`** -- Resolves customization from three-layer TOML merge (user > team > defaults). Outputs JSON. + ### Step 1: Resolve Activation Customization Resolve `persona`, `inject`, `additional_resources`, and `menu` from customization: -Run: `python ./scripts/resolve-customization.py gds-agent-tech-writer --key persona --key inject --key additional_resources --key menu` +Run: `python3 scripts/resolve-customization.py gds-agent-tech-writer --key persona --key inject --key additional_resources --key menu` Use the JSON output as resolved values. ### Step 2: Apply Customization diff --git a/src/workflows/1-preproduction/gds-brainstorm-game/SKILL.md b/src/workflows/1-preproduction/gds-brainstorm-game/SKILL.md index 1e36668..4dff7db 100644 --- a/src/workflows/1-preproduction/gds-brainstorm-game/SKILL.md +++ b/src/workflows/1-preproduction/gds-brainstorm-game/SKILL.md @@ -3,10 +3,14 @@ name: gds-brainstorm-game description: 'Facilitate game brainstorming sessions with game-specific techniques. Use when the user says "brainstorm game" or "game ideas"' --- +## 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: `python ./scripts/resolve-customization.py gds-brainstorm-game --key inject --key additional_resources` +Run: `python3 scripts/resolve-customization.py gds-brainstorm-game --key inject --key additional_resources` Use the JSON output as resolved values. If `inject.before` is not empty, incorporate its content as high-priority context. @@ -17,6 +21,6 @@ Follow the instructions in ./workflow.md. ## Post-Workflow Customization After the workflow completes, resolve `inject.after` from customization: -Run: `python ./scripts/resolve-customization.py gds-brainstorm-game --key inject.after` +Run: `python3 scripts/resolve-customization.py gds-brainstorm-game --key inject.after` If resolved `inject.after` is not empty, incorporate its content as a final checklist or validation gate. diff --git a/src/workflows/1-preproduction/gds-create-game-brief/SKILL.md b/src/workflows/1-preproduction/gds-create-game-brief/SKILL.md index 6aafc07..ce990de 100644 --- a/src/workflows/1-preproduction/gds-create-game-brief/SKILL.md +++ b/src/workflows/1-preproduction/gds-create-game-brief/SKILL.md @@ -3,10 +3,14 @@ name: gds-create-game-brief description: 'Interactive game brief creation guiding users through defining their game vision. Use when the user says "game brief" or "create brief"' --- +## 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: `python ./scripts/resolve-customization.py gds-create-game-brief --key inject --key additional_resources` +Run: `python3 scripts/resolve-customization.py gds-create-game-brief --key inject --key additional_resources` Use the JSON output as resolved values. If `inject.before` is not empty, incorporate its content as high-priority context. @@ -17,6 +21,6 @@ Follow the instructions in ./workflow.md. ## Post-Workflow Customization After the workflow completes, resolve `inject.after` from customization: -Run: `python ./scripts/resolve-customization.py gds-create-game-brief --key inject.after` +Run: `python3 scripts/resolve-customization.py gds-create-game-brief --key inject.after` If resolved `inject.after` is not empty, incorporate its content as a final checklist or validation gate. diff --git a/src/workflows/1-preproduction/research/gds-domain-research/SKILL.md b/src/workflows/1-preproduction/research/gds-domain-research/SKILL.md index 70c2fb3..7b36216 100644 --- a/src/workflows/1-preproduction/research/gds-domain-research/SKILL.md +++ b/src/workflows/1-preproduction/research/gds-domain-research/SKILL.md @@ -3,10 +3,14 @@ name: gds-domain-research description: 'Conduct game domain and industry research. Use when the user says "lets create a research report on [game domain or industry]"' --- +## 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: `python ./scripts/resolve-customization.py gds-domain-research --key inject --key additional_resources` +Run: `python3 scripts/resolve-customization.py gds-domain-research --key inject --key additional_resources` Use the JSON output as resolved values. If `inject.before` is not empty, incorporate its content as high-priority context. @@ -17,6 +21,6 @@ Follow the instructions in ./workflow.md. ## Post-Workflow Customization After the workflow completes, resolve `inject.after` from customization: -Run: `python ./scripts/resolve-customization.py gds-domain-research --key inject.after` +Run: `python3 scripts/resolve-customization.py gds-domain-research --key inject.after` If resolved `inject.after` is not empty, incorporate its content as a final checklist or validation gate. diff --git a/src/workflows/2-design/gds-create-gdd/SKILL.md b/src/workflows/2-design/gds-create-gdd/SKILL.md index 78f4862..9767163 100644 --- a/src/workflows/2-design/gds-create-gdd/SKILL.md +++ b/src/workflows/2-design/gds-create-gdd/SKILL.md @@ -3,10 +3,14 @@ name: gds-create-gdd description: 'Create Game Design Documents with mechanics and implementation guidance. Use when the user says "create GDD" or "game design document"' --- +## 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: `python ./scripts/resolve-customization.py gds-create-gdd --key inject --key additional_resources` +Run: `python3 scripts/resolve-customization.py gds-create-gdd --key inject --key additional_resources` Use the JSON output as resolved values. If `inject.before` is not empty, incorporate its content as high-priority context. @@ -17,6 +21,6 @@ Follow the instructions in ./workflow.md. ## Post-Workflow Customization After the workflow completes, resolve `inject.after` from customization: -Run: `python ./scripts/resolve-customization.py gds-create-gdd --key inject.after` +Run: `python3 scripts/resolve-customization.py gds-create-gdd --key inject.after` If resolved `inject.after` is not empty, incorporate its content as a final checklist or validation gate. diff --git a/src/workflows/2-design/gds-create-narrative/SKILL.md b/src/workflows/2-design/gds-create-narrative/SKILL.md index ba18192..d2928d0 100644 --- a/src/workflows/2-design/gds-create-narrative/SKILL.md +++ b/src/workflows/2-design/gds-create-narrative/SKILL.md @@ -3,10 +3,14 @@ name: gds-create-narrative description: 'Create comprehensive narrative documentation with story structure and world-building. Use when the user says "narrative design" or "create narrative"' --- +## 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: `python ./scripts/resolve-customization.py gds-create-narrative --key inject --key additional_resources` +Run: `python3 scripts/resolve-customization.py gds-create-narrative --key inject --key additional_resources` Use the JSON output as resolved values. If `inject.before` is not empty, incorporate its content as high-priority context. @@ -17,6 +21,6 @@ Follow the instructions in ./workflow.md. ## Post-Workflow Customization After the workflow completes, resolve `inject.after` from customization: -Run: `python ./scripts/resolve-customization.py gds-create-narrative --key inject.after` +Run: `python3 scripts/resolve-customization.py gds-create-narrative --key inject.after` If resolved `inject.after` is not empty, incorporate its content as a final checklist or validation gate. diff --git a/src/workflows/2-design/gds-create-ux-design/SKILL.md b/src/workflows/2-design/gds-create-ux-design/SKILL.md index 1287c5d..618abc6 100644 --- a/src/workflows/2-design/gds-create-ux-design/SKILL.md +++ b/src/workflows/2-design/gds-create-ux-design/SKILL.md @@ -3,10 +3,14 @@ name: gds-create-ux-design description: 'Plan UX patterns and design specifications for game UI/HUD elements. Use when the user says "lets create UX design" or "create UX specifications" or "help me plan the game UX"' --- +## 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: `python ./scripts/resolve-customization.py gds-create-ux-design --key inject --key additional_resources` +Run: `python3 scripts/resolve-customization.py gds-create-ux-design --key inject --key additional_resources` Use the JSON output as resolved values. If `inject.before` is not empty, incorporate its content as high-priority context. @@ -17,6 +21,6 @@ Follow the instructions in ./workflow.md. ## Post-Workflow Customization After the workflow completes, resolve `inject.after` from customization: -Run: `python ./scripts/resolve-customization.py gds-create-ux-design --key inject.after` +Run: `python3 scripts/resolve-customization.py gds-create-ux-design --key inject.after` If resolved `inject.after` is not empty, incorporate its content as a final checklist or validation gate. diff --git a/src/workflows/3-technical/gds-check-implementation-readiness/SKILL.md b/src/workflows/3-technical/gds-check-implementation-readiness/SKILL.md index e5f0321..129080c 100644 --- a/src/workflows/3-technical/gds-check-implementation-readiness/SKILL.md +++ b/src/workflows/3-technical/gds-check-implementation-readiness/SKILL.md @@ -3,10 +3,14 @@ name: gds-check-implementation-readiness description: 'Verify GDD, UX, Architecture, and Epics alignment before production. Use when the user says "check readiness" or "implementation readiness"' --- +## 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: `python ./scripts/resolve-customization.py gds-check-implementation-readiness --key inject --key additional_resources` +Run: `python3 scripts/resolve-customization.py gds-check-implementation-readiness --key inject --key additional_resources` Use the JSON output as resolved values. If `inject.before` is not empty, incorporate its content as high-priority context. @@ -17,6 +21,6 @@ Follow the instructions in ./workflow.md. ## Post-Workflow Customization After the workflow completes, resolve `inject.after` from customization: -Run: `python ./scripts/resolve-customization.py gds-check-implementation-readiness --key inject.after` +Run: `python3 scripts/resolve-customization.py gds-check-implementation-readiness --key inject.after` If resolved `inject.after` is not empty, incorporate its content as a final checklist or validation gate. diff --git a/src/workflows/3-technical/gds-create-epics-and-stories/SKILL.md b/src/workflows/3-technical/gds-create-epics-and-stories/SKILL.md index 7a16e79..7e4d757 100644 --- a/src/workflows/3-technical/gds-create-epics-and-stories/SKILL.md +++ b/src/workflows/3-technical/gds-create-epics-and-stories/SKILL.md @@ -3,10 +3,14 @@ name: gds-create-epics-and-stories description: 'Create Epics and Stories from GDD requirements for development. Use when the user says "create epics" or "create stories"' --- +## 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: `python ./scripts/resolve-customization.py gds-create-epics-and-stories --key inject --key additional_resources` +Run: `python3 scripts/resolve-customization.py gds-create-epics-and-stories --key inject --key additional_resources` Use the JSON output as resolved values. If `inject.before` is not empty, incorporate its content as high-priority context. @@ -17,6 +21,6 @@ Follow the instructions in ./workflow.md. ## Post-Workflow Customization After the workflow completes, resolve `inject.after` from customization: -Run: `python ./scripts/resolve-customization.py gds-create-epics-and-stories --key inject.after` +Run: `python3 scripts/resolve-customization.py gds-create-epics-and-stories --key inject.after` If resolved `inject.after` is not empty, incorporate its content as a final checklist or validation gate. diff --git a/src/workflows/3-technical/gds-game-architecture/SKILL.md b/src/workflows/3-technical/gds-game-architecture/SKILL.md index 057d307..93da3ce 100644 --- a/src/workflows/3-technical/gds-game-architecture/SKILL.md +++ b/src/workflows/3-technical/gds-game-architecture/SKILL.md @@ -3,10 +3,14 @@ name: gds-game-architecture description: 'Design scale-adaptive game architecture with engine systems and networking. Use when the user says "game architecture" or "design architecture"' --- +## 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: `python ./scripts/resolve-customization.py gds-game-architecture --key inject --key additional_resources` +Run: `python3 scripts/resolve-customization.py gds-game-architecture --key inject --key additional_resources` Use the JSON output as resolved values. If `inject.before` is not empty, incorporate its content as high-priority context. @@ -17,6 +21,6 @@ Follow the instructions in ./workflow.md. ## Post-Workflow Customization After the workflow completes, resolve `inject.after` from customization: -Run: `python ./scripts/resolve-customization.py gds-game-architecture --key inject.after` +Run: `python3 scripts/resolve-customization.py gds-game-architecture --key inject.after` If resolved `inject.after` is not empty, incorporate its content as a final checklist or validation gate. diff --git a/src/workflows/3-technical/gds-generate-project-context/SKILL.md b/src/workflows/3-technical/gds-generate-project-context/SKILL.md index 2c70c59..843b31a 100644 --- a/src/workflows/3-technical/gds-generate-project-context/SKILL.md +++ b/src/workflows/3-technical/gds-generate-project-context/SKILL.md @@ -3,10 +3,14 @@ name: gds-generate-project-context description: 'Create optimized project-context.md for AI agent consistency. Use when the user says "project context" or "generate context"' --- +## 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: `python ./scripts/resolve-customization.py gds-generate-project-context --key inject --key additional_resources` +Run: `python3 scripts/resolve-customization.py gds-generate-project-context --key inject --key additional_resources` Use the JSON output as resolved values. If `inject.before` is not empty, incorporate its content as high-priority context. @@ -17,6 +21,6 @@ Follow the instructions in ./workflow.md. ## Post-Workflow Customization After the workflow completes, resolve `inject.after` from customization: -Run: `python ./scripts/resolve-customization.py gds-generate-project-context --key inject.after` +Run: `python3 scripts/resolve-customization.py gds-generate-project-context --key inject.after` If resolved `inject.after` is not empty, incorporate its content as a final checklist or validation gate. diff --git a/src/workflows/4-production/gds-code-review/SKILL.md b/src/workflows/4-production/gds-code-review/SKILL.md index be0d8f9..76d0a4f 100644 --- a/src/workflows/4-production/gds-code-review/SKILL.md +++ b/src/workflows/4-production/gds-code-review/SKILL.md @@ -3,10 +3,14 @@ name: gds-code-review description: 'Perform thorough code review to find bugs and quality issues. Use when the user says "review this code" or "do a code review"' --- +## 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: `python ./scripts/resolve-customization.py gds-code-review --key inject --key additional_resources` +Run: `python3 scripts/resolve-customization.py gds-code-review --key inject --key additional_resources` Use the JSON output as resolved values. If `inject.before` is not empty, incorporate its content as high-priority context. @@ -17,6 +21,6 @@ Follow the instructions in ./workflow.md. ## Post-Workflow Customization After the workflow completes, resolve `inject.after` from customization: -Run: `python ./scripts/resolve-customization.py gds-code-review --key inject.after` +Run: `python3 scripts/resolve-customization.py gds-code-review --key inject.after` If resolved `inject.after` is not empty, incorporate its content as a final checklist or validation gate. diff --git a/src/workflows/4-production/gds-correct-course/SKILL.md b/src/workflows/4-production/gds-correct-course/SKILL.md index 3193096..39e7229 100644 --- a/src/workflows/4-production/gds-correct-course/SKILL.md +++ b/src/workflows/4-production/gds-correct-course/SKILL.md @@ -3,10 +3,14 @@ name: gds-correct-course description: 'Manage significant changes when sprint implementation is off track. Use when the user says "correct course" or "we need a course correction"' --- +## 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: `python ./scripts/resolve-customization.py gds-correct-course --key inject --key additional_resources` +Run: `python3 scripts/resolve-customization.py gds-correct-course --key inject --key additional_resources` Use the JSON output as resolved values. If `inject.before` is not empty, incorporate its content as high-priority context. @@ -17,6 +21,6 @@ Follow the instructions in ./workflow.md. ## Post-Workflow Customization After the workflow completes, resolve `inject.after` from customization: -Run: `python ./scripts/resolve-customization.py gds-correct-course --key inject.after` +Run: `python3 scripts/resolve-customization.py gds-correct-course --key inject.after` If resolved `inject.after` is not empty, incorporate its content as a final checklist or validation gate. diff --git a/src/workflows/4-production/gds-create-story/SKILL.md b/src/workflows/4-production/gds-create-story/SKILL.md index 0146c22..b568978 100644 --- a/src/workflows/4-production/gds-create-story/SKILL.md +++ b/src/workflows/4-production/gds-create-story/SKILL.md @@ -3,10 +3,14 @@ name: gds-create-story description: 'Creates a dedicated story file with all the context the agent will need to implement it later. Use when the user says "create the next story" or "create story [story identifier]"' --- +## 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: `python ./scripts/resolve-customization.py gds-create-story --key inject --key additional_resources` +Run: `python3 scripts/resolve-customization.py gds-create-story --key inject --key additional_resources` Use the JSON output as resolved values. If `inject.before` is not empty, incorporate its content as high-priority context. @@ -17,6 +21,6 @@ Follow the instructions in ./workflow.md. ## Post-Workflow Customization After the workflow completes, resolve `inject.after` from customization: -Run: `python ./scripts/resolve-customization.py gds-create-story --key inject.after` +Run: `python3 scripts/resolve-customization.py gds-create-story --key inject.after` If resolved `inject.after` is not empty, incorporate its content as a final checklist or validation gate. diff --git a/src/workflows/4-production/gds-dev-story/SKILL.md b/src/workflows/4-production/gds-dev-story/SKILL.md index bb3f684..5db5859 100644 --- a/src/workflows/4-production/gds-dev-story/SKILL.md +++ b/src/workflows/4-production/gds-dev-story/SKILL.md @@ -3,10 +3,14 @@ name: gds-dev-story description: 'Execute story implementation following a context filled story spec file. Use when the user says "dev this story [story file]" or "implement the next story in the sprint plan"' --- +## 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: `python ./scripts/resolve-customization.py gds-dev-story --key inject --key additional_resources` +Run: `python3 scripts/resolve-customization.py gds-dev-story --key inject --key additional_resources` Use the JSON output as resolved values. If `inject.before` is not empty, incorporate its content as high-priority context. @@ -17,6 +21,6 @@ Follow the instructions in ./workflow.md. ## Post-Workflow Customization After the workflow completes, resolve `inject.after` from customization: -Run: `python ./scripts/resolve-customization.py gds-dev-story --key inject.after` +Run: `python3 scripts/resolve-customization.py gds-dev-story --key inject.after` If resolved `inject.after` is not empty, incorporate its content as a final checklist or validation gate. diff --git a/src/workflows/4-production/gds-retrospective/SKILL.md b/src/workflows/4-production/gds-retrospective/SKILL.md index ce5eb53..746bacc 100644 --- a/src/workflows/4-production/gds-retrospective/SKILL.md +++ b/src/workflows/4-production/gds-retrospective/SKILL.md @@ -3,10 +3,14 @@ name: gds-retrospective description: 'Facilitate a retrospective after completing a game development epic. Use when the user says "run a retrospective" or "lets do a retro"' --- +## 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: `python ./scripts/resolve-customization.py gds-retrospective --key inject --key additional_resources` +Run: `python3 scripts/resolve-customization.py gds-retrospective --key inject --key additional_resources` Use the JSON output as resolved values. If `inject.before` is not empty, incorporate its content as high-priority context. @@ -17,6 +21,6 @@ Follow the instructions in ./workflow.md. ## Post-Workflow Customization After the workflow completes, resolve `inject.after` from customization: -Run: `python ./scripts/resolve-customization.py gds-retrospective --key inject.after` +Run: `python3 scripts/resolve-customization.py gds-retrospective --key inject.after` If resolved `inject.after` is not empty, incorporate its content as a final checklist or validation gate. diff --git a/src/workflows/4-production/gds-sprint-planning/SKILL.md b/src/workflows/4-production/gds-sprint-planning/SKILL.md index 9fb1956..68baaef 100644 --- a/src/workflows/4-production/gds-sprint-planning/SKILL.md +++ b/src/workflows/4-production/gds-sprint-planning/SKILL.md @@ -3,10 +3,14 @@ name: gds-sprint-planning description: 'Generate or update sprint status from epic files for sprint planning. Use when the user says "plan the sprint" or "create sprint plan"' --- +## 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: `python ./scripts/resolve-customization.py gds-sprint-planning --key inject --key additional_resources` +Run: `python3 scripts/resolve-customization.py gds-sprint-planning --key inject --key additional_resources` Use the JSON output as resolved values. If `inject.before` is not empty, incorporate its content as high-priority context. @@ -17,6 +21,6 @@ Follow the instructions in ./workflow.md. ## Post-Workflow Customization After the workflow completes, resolve `inject.after` from customization: -Run: `python ./scripts/resolve-customization.py gds-sprint-planning --key inject.after` +Run: `python3 scripts/resolve-customization.py gds-sprint-planning --key inject.after` If resolved `inject.after` is not empty, incorporate its content as a final checklist or validation gate. diff --git a/src/workflows/4-production/gds-sprint-status/SKILL.md b/src/workflows/4-production/gds-sprint-status/SKILL.md index fb867b9..8213077 100644 --- a/src/workflows/4-production/gds-sprint-status/SKILL.md +++ b/src/workflows/4-production/gds-sprint-status/SKILL.md @@ -3,10 +3,14 @@ name: gds-sprint-status description: 'Summarize current sprint progress and surface risks. Use when the user says "sprint status" or "how is the sprint going"' --- +## 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: `python ./scripts/resolve-customization.py gds-sprint-status --key inject --key additional_resources` +Run: `python3 scripts/resolve-customization.py gds-sprint-status --key inject --key additional_resources` Use the JSON output as resolved values. If `inject.before` is not empty, incorporate its content as high-priority context. @@ -17,6 +21,6 @@ Follow the instructions in ./workflow.md. ## Post-Workflow Customization After the workflow completes, resolve `inject.after` from customization: -Run: `python ./scripts/resolve-customization.py gds-sprint-status --key inject.after` +Run: `python3 scripts/resolve-customization.py gds-sprint-status --key inject.after` If resolved `inject.after` is not empty, incorporate its content as a final checklist or validation gate. diff --git a/src/workflows/gametest/gds-e2e-scaffold/SKILL.md b/src/workflows/gametest/gds-e2e-scaffold/SKILL.md index 0b1ef6b..30f8170 100644 --- a/src/workflows/gametest/gds-e2e-scaffold/SKILL.md +++ b/src/workflows/gametest/gds-e2e-scaffold/SKILL.md @@ -3,10 +3,14 @@ name: gds-e2e-scaffold description: 'Scaffold end-to-end testing infrastructure. Use when the user says "e2e scaffold" or "set up e2e testing"' --- +## 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: `python ./scripts/resolve-customization.py gds-e2e-scaffold --key inject --key additional_resources` +Run: `python3 scripts/resolve-customization.py gds-e2e-scaffold --key inject --key additional_resources` Use the JSON output as resolved values. If `inject.before` is not empty, incorporate its content as high-priority context. @@ -17,6 +21,6 @@ Follow the instructions in ./workflow.md. ## Post-Workflow Customization After the workflow completes, resolve `inject.after` from customization: -Run: `python ./scripts/resolve-customization.py gds-e2e-scaffold --key inject.after` +Run: `python3 scripts/resolve-customization.py gds-e2e-scaffold --key inject.after` If resolved `inject.after` is not empty, incorporate its content as a final checklist or validation gate. diff --git a/src/workflows/gametest/gds-performance-test/SKILL.md b/src/workflows/gametest/gds-performance-test/SKILL.md index 5ae6ef1..75e9bfe 100644 --- a/src/workflows/gametest/gds-performance-test/SKILL.md +++ b/src/workflows/gametest/gds-performance-test/SKILL.md @@ -3,10 +3,14 @@ name: gds-performance-test description: 'Design game performance testing strategy. Use when the user says "performance test" or "benchmark"' --- +## 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: `python ./scripts/resolve-customization.py gds-performance-test --key inject --key additional_resources` +Run: `python3 scripts/resolve-customization.py gds-performance-test --key inject --key additional_resources` Use the JSON output as resolved values. If `inject.before` is not empty, incorporate its content as high-priority context. @@ -17,6 +21,6 @@ Follow the instructions in ./workflow.md. ## Post-Workflow Customization After the workflow completes, resolve `inject.after` from customization: -Run: `python ./scripts/resolve-customization.py gds-performance-test --key inject.after` +Run: `python3 scripts/resolve-customization.py gds-performance-test --key inject.after` If resolved `inject.after` is not empty, incorporate its content as a final checklist or validation gate. diff --git a/src/workflows/gametest/gds-playtest-plan/SKILL.md b/src/workflows/gametest/gds-playtest-plan/SKILL.md index 112a5c2..68af6d9 100644 --- a/src/workflows/gametest/gds-playtest-plan/SKILL.md +++ b/src/workflows/gametest/gds-playtest-plan/SKILL.md @@ -3,10 +3,14 @@ name: gds-playtest-plan description: 'Create structured playtesting plans for user feedback. Use when the user says "playtest plan" or "playtesting"' --- +## 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: `python ./scripts/resolve-customization.py gds-playtest-plan --key inject --key additional_resources` +Run: `python3 scripts/resolve-customization.py gds-playtest-plan --key inject --key additional_resources` Use the JSON output as resolved values. If `inject.before` is not empty, incorporate its content as high-priority context. @@ -17,6 +21,6 @@ Follow the instructions in ./workflow.md. ## Post-Workflow Customization After the workflow completes, resolve `inject.after` from customization: -Run: `python ./scripts/resolve-customization.py gds-playtest-plan --key inject.after` +Run: `python3 scripts/resolve-customization.py gds-playtest-plan --key inject.after` If resolved `inject.after` is not empty, incorporate its content as a final checklist or validation gate. diff --git a/src/workflows/gametest/gds-test-automate/SKILL.md b/src/workflows/gametest/gds-test-automate/SKILL.md index fa0dfc1..b2cdc94 100644 --- a/src/workflows/gametest/gds-test-automate/SKILL.md +++ b/src/workflows/gametest/gds-test-automate/SKILL.md @@ -3,10 +3,14 @@ name: gds-test-automate description: 'Generate automated game tests for gameplay systems. Use when the user says "automate tests" or "generate tests"' --- +## 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: `python ./scripts/resolve-customization.py gds-test-automate --key inject --key additional_resources` +Run: `python3 scripts/resolve-customization.py gds-test-automate --key inject --key additional_resources` Use the JSON output as resolved values. If `inject.before` is not empty, incorporate its content as high-priority context. @@ -17,6 +21,6 @@ Follow the instructions in ./workflow.md. ## Post-Workflow Customization After the workflow completes, resolve `inject.after` from customization: -Run: `python ./scripts/resolve-customization.py gds-test-automate --key inject.after` +Run: `python3 scripts/resolve-customization.py gds-test-automate --key inject.after` If resolved `inject.after` is not empty, incorporate its content as a final checklist or validation gate. diff --git a/src/workflows/gametest/gds-test-design/SKILL.md b/src/workflows/gametest/gds-test-design/SKILL.md index 2960fae..fc5beec 100644 --- a/src/workflows/gametest/gds-test-design/SKILL.md +++ b/src/workflows/gametest/gds-test-design/SKILL.md @@ -3,10 +3,14 @@ name: gds-test-design description: 'Create comprehensive game test scenarios. Use when the user says "test design" or "design tests"' --- +## 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: `python ./scripts/resolve-customization.py gds-test-design --key inject --key additional_resources` +Run: `python3 scripts/resolve-customization.py gds-test-design --key inject --key additional_resources` Use the JSON output as resolved values. If `inject.before` is not empty, incorporate its content as high-priority context. @@ -17,6 +21,6 @@ Follow the instructions in ./workflow.md. ## Post-Workflow Customization After the workflow completes, resolve `inject.after` from customization: -Run: `python ./scripts/resolve-customization.py gds-test-design --key inject.after` +Run: `python3 scripts/resolve-customization.py gds-test-design --key inject.after` If resolved `inject.after` is not empty, incorporate its content as a final checklist or validation gate. diff --git a/src/workflows/gametest/gds-test-framework/SKILL.md b/src/workflows/gametest/gds-test-framework/SKILL.md index 022b9d6..6629abc 100644 --- a/src/workflows/gametest/gds-test-framework/SKILL.md +++ b/src/workflows/gametest/gds-test-framework/SKILL.md @@ -3,10 +3,14 @@ name: gds-test-framework description: 'Initialize game test framework for Unity, Unreal, or Godot. Use when the user says "test framework" or "set up testing"' --- +## 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: `python ./scripts/resolve-customization.py gds-test-framework --key inject --key additional_resources` +Run: `python3 scripts/resolve-customization.py gds-test-framework --key inject --key additional_resources` Use the JSON output as resolved values. If `inject.before` is not empty, incorporate its content as high-priority context. @@ -17,6 +21,6 @@ Follow the instructions in ./workflow.md. ## Post-Workflow Customization After the workflow completes, resolve `inject.after` from customization: -Run: `python ./scripts/resolve-customization.py gds-test-framework --key inject.after` +Run: `python3 scripts/resolve-customization.py gds-test-framework --key inject.after` If resolved `inject.after` is not empty, incorporate its content as a final checklist or validation gate. diff --git a/src/workflows/gametest/gds-test-review/SKILL.md b/src/workflows/gametest/gds-test-review/SKILL.md index 243ab5c..4436ef4 100644 --- a/src/workflows/gametest/gds-test-review/SKILL.md +++ b/src/workflows/gametest/gds-test-review/SKILL.md @@ -3,10 +3,14 @@ name: gds-test-review description: 'Review test quality and coverage. Use when the user says "test review" or "review tests"' --- +## 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: `python ./scripts/resolve-customization.py gds-test-review --key inject --key additional_resources` +Run: `python3 scripts/resolve-customization.py gds-test-review --key inject --key additional_resources` Use the JSON output as resolved values. If `inject.before` is not empty, incorporate its content as high-priority context. @@ -17,6 +21,6 @@ Follow the instructions in ./workflow.md. ## Post-Workflow Customization After the workflow completes, resolve `inject.after` from customization: -Run: `python ./scripts/resolve-customization.py gds-test-review --key inject.after` +Run: `python3 scripts/resolve-customization.py gds-test-review --key inject.after` If resolved `inject.after` is not empty, incorporate its content as a final checklist or validation gate. diff --git a/src/workflows/gds-document-project/SKILL.md b/src/workflows/gds-document-project/SKILL.md index 3f6e966..8c4c241 100644 --- a/src/workflows/gds-document-project/SKILL.md +++ b/src/workflows/gds-document-project/SKILL.md @@ -3,10 +3,14 @@ name: gds-document-project description: 'Analyze existing game projects to produce useful documentation. Use when the user says "document project" or "generate docs"' --- +## 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: `python ./scripts/resolve-customization.py gds-document-project --key inject --key additional_resources` +Run: `python3 scripts/resolve-customization.py gds-document-project --key inject --key additional_resources` Use the JSON output as resolved values. If `inject.before` is not empty, incorporate its content as high-priority context. @@ -17,6 +21,6 @@ Follow the instructions in ./workflow.md. ## Post-Workflow Customization After the workflow completes, resolve `inject.after` from customization: -Run: `python ./scripts/resolve-customization.py gds-document-project --key inject.after` +Run: `python3 scripts/resolve-customization.py gds-document-project --key inject.after` If resolved `inject.after` is not empty, incorporate its content as a final checklist or validation gate. diff --git a/src/workflows/gds-quick-flow/gds-quick-dev-new-preview/SKILL.md b/src/workflows/gds-quick-flow/gds-quick-dev-new-preview/SKILL.md index e549e23..154cdd3 100644 --- a/src/workflows/gds-quick-flow/gds-quick-dev-new-preview/SKILL.md +++ b/src/workflows/gds-quick-flow/gds-quick-dev-new-preview/SKILL.md @@ -3,10 +3,14 @@ name: gds-quick-dev-new-preview description: 'Implements any user intent, GDD requirement, story, bug fix or change request by producing clean working code artifacts that follow the project''s existing game architecture, patterns and conventions. Use when the user wants to build, fix, tweak, refactor, add or modify any game code, component or feature.' --- +## 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: `python ./scripts/resolve-customization.py gds-quick-dev-new-preview --key inject --key additional_resources` +Run: `python3 scripts/resolve-customization.py gds-quick-dev-new-preview --key inject --key additional_resources` Use the JSON output as resolved values. If `inject.before` is not empty, incorporate its content as high-priority context. @@ -17,6 +21,6 @@ Follow the instructions in ./workflow.md. ## Post-Workflow Customization After the workflow completes, resolve `inject.after` from customization: -Run: `python ./scripts/resolve-customization.py gds-quick-dev-new-preview --key inject.after` +Run: `python3 scripts/resolve-customization.py gds-quick-dev-new-preview --key inject.after` If resolved `inject.after` is not empty, incorporate its content as a final checklist or validation gate. diff --git a/src/workflows/gds-quick-flow/gds-quick-dev/SKILL.md b/src/workflows/gds-quick-flow/gds-quick-dev/SKILL.md index 4d20c03..238439a 100644 --- a/src/workflows/gds-quick-flow/gds-quick-dev/SKILL.md +++ b/src/workflows/gds-quick-flow/gds-quick-dev/SKILL.md @@ -3,10 +3,14 @@ name: gds-quick-dev description: 'Flexible development workflow - execute tech-specs OR direct instructions with optional planning. Use when the user says "lets implement this feature" or "execute these development tasks"' --- +## 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: `python ./scripts/resolve-customization.py gds-quick-dev --key inject --key additional_resources` +Run: `python3 scripts/resolve-customization.py gds-quick-dev --key inject --key additional_resources` Use the JSON output as resolved values. If `inject.before` is not empty, incorporate its content as high-priority context. @@ -17,6 +21,6 @@ Follow the instructions in ./workflow.md. ## Post-Workflow Customization After the workflow completes, resolve `inject.after` from customization: -Run: `python ./scripts/resolve-customization.py gds-quick-dev --key inject.after` +Run: `python3 scripts/resolve-customization.py gds-quick-dev --key inject.after` If resolved `inject.after` is not empty, incorporate its content as a final checklist or validation gate. diff --git a/src/workflows/gds-quick-flow/gds-quick-spec/SKILL.md b/src/workflows/gds-quick-flow/gds-quick-spec/SKILL.md index c82d639..283c260 100644 --- a/src/workflows/gds-quick-flow/gds-quick-spec/SKILL.md +++ b/src/workflows/gds-quick-flow/gds-quick-spec/SKILL.md @@ -3,10 +3,14 @@ name: gds-quick-spec description: 'Create technical specs with implementation-ready stories. Use when the user says "quick spec" or "tech spec"' --- +## 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: `python ./scripts/resolve-customization.py gds-quick-spec --key inject --key additional_resources` +Run: `python3 scripts/resolve-customization.py gds-quick-spec --key inject --key additional_resources` Use the JSON output as resolved values. If `inject.before` is not empty, incorporate its content as high-priority context. @@ -17,6 +21,6 @@ Follow the instructions in ./workflow.md. ## Post-Workflow Customization After the workflow completes, resolve `inject.after` from customization: -Run: `python ./scripts/resolve-customization.py gds-quick-spec --key inject.after` +Run: `python3 scripts/resolve-customization.py gds-quick-spec --key inject.after` If resolved `inject.after` is not empty, incorporate its content as a final checklist or validation gate. From 95ba0038e90edf07333a36c7046275d217e32202 Mon Sep 17 00:00:00 2001 From: Brian Madison Date: Tue, 14 Apr 2026 13:27:57 -0500 Subject: [PATCH 5/7] fix: address review findings from code review - Fix merge_menu KeyError crash when menu items missing 'code' key - Fix _is_menu_array to check ALL elements, not just first - Remove unused import os from resolve-customization.py - Remove inject.after from agent activation and customize.toml --- src/agents/gds-agent-game-architect/SKILL.md | 2 -- src/agents/gds-agent-game-architect/customize.toml | 1 - .../scripts/resolve-customization.py | 11 ++++++----- src/agents/gds-agent-game-designer/SKILL.md | 2 -- src/agents/gds-agent-game-designer/customize.toml | 1 - .../scripts/resolve-customization.py | 11 ++++++----- src/agents/gds-agent-game-dev/SKILL.md | 2 -- src/agents/gds-agent-game-dev/customize.toml | 1 - .../scripts/resolve-customization.py | 11 ++++++----- src/agents/gds-agent-game-qa/SKILL.md | 2 -- src/agents/gds-agent-game-qa/customize.toml | 1 - .../scripts/resolve-customization.py | 11 ++++++----- src/agents/gds-agent-game-scrum-master/SKILL.md | 2 -- src/agents/gds-agent-game-scrum-master/customize.toml | 1 - .../scripts/resolve-customization.py | 11 ++++++----- src/agents/gds-agent-game-solo-dev/SKILL.md | 2 -- src/agents/gds-agent-game-solo-dev/customize.toml | 1 - .../scripts/resolve-customization.py | 11 ++++++----- src/agents/gds-agent-tech-writer/SKILL.md | 2 -- src/agents/gds-agent-tech-writer/customize.toml | 1 - .../scripts/resolve-customization.py | 11 ++++++----- .../scripts/resolve-customization.py | 11 ++++++----- .../scripts/resolve-customization.py | 11 ++++++----- .../scripts/resolve-customization.py | 11 ++++++----- .../gds-create-gdd/scripts/resolve-customization.py | 11 ++++++----- .../scripts/resolve-customization.py | 11 ++++++----- .../scripts/resolve-customization.py | 11 ++++++----- .../scripts/resolve-customization.py | 11 ++++++----- .../scripts/resolve-customization.py | 11 ++++++----- .../scripts/resolve-customization.py | 11 ++++++----- .../scripts/resolve-customization.py | 11 ++++++----- .../gds-code-review/scripts/resolve-customization.py | 11 ++++++----- .../scripts/resolve-customization.py | 11 ++++++----- .../gds-create-story/scripts/resolve-customization.py | 11 ++++++----- .../gds-dev-story/scripts/resolve-customization.py | 11 ++++++----- .../scripts/resolve-customization.py | 11 ++++++----- .../scripts/resolve-customization.py | 11 ++++++----- .../scripts/resolve-customization.py | 11 ++++++----- .../gds-e2e-scaffold/scripts/resolve-customization.py | 11 ++++++----- .../scripts/resolve-customization.py | 11 ++++++----- .../scripts/resolve-customization.py | 11 ++++++----- .../scripts/resolve-customization.py | 11 ++++++----- .../gds-test-design/scripts/resolve-customization.py | 11 ++++++----- .../scripts/resolve-customization.py | 11 ++++++----- .../gds-test-review/scripts/resolve-customization.py | 11 ++++++----- .../scripts/resolve-customization.py | 11 ++++++----- .../scripts/resolve-customization.py | 11 ++++++----- .../gds-quick-dev/scripts/resolve-customization.py | 11 ++++++----- .../gds-quick-spec/scripts/resolve-customization.py | 11 ++++++----- 49 files changed, 210 insertions(+), 196 deletions(-) diff --git a/src/agents/gds-agent-game-architect/SKILL.md b/src/agents/gds-agent-game-architect/SKILL.md index 5f4ee72..daa34b5 100644 --- a/src/agents/gds-agent-game-architect/SKILL.md +++ b/src/agents/gds-agent-game-architect/SKILL.md @@ -24,8 +24,6 @@ Use the JSON output as resolved values. 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. -4. **Inject after** -- If `inject.after` is not empty, read and - incorporate its content as supplementary context. 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. diff --git a/src/agents/gds-agent-game-architect/customize.toml b/src/agents/gds-agent-game-architect/customize.toml index e572ac5..208ca20 100644 --- a/src/agents/gds-agent-game-architect/customize.toml +++ b/src/agents/gds-agent-game-architect/customize.toml @@ -53,4 +53,3 @@ Architecture is about delaying decisions until you have enough data. Build for t # ────────────────────────────────────────────────────────────────── [inject] before = "" -after = "" diff --git a/src/agents/gds-agent-game-architect/scripts/resolve-customization.py b/src/agents/gds-agent-game-architect/scripts/resolve-customization.py index 6bbce66..c102b15 100755 --- a/src/agents/gds-agent-game-architect/scripts/resolve-customization.py +++ b/src/agents/gds-agent-game-architect/scripts/resolve-customization.py @@ -21,7 +21,6 @@ import argparse import json -import os import sys import tomllib from pathlib import Path @@ -57,19 +56,21 @@ def load_toml(path: Path) -> dict[str, Any]: # --------------------------------------------------------------------------- def _is_menu_array(value: Any) -> bool: - """True when *value* looks like a ``[[menu]]`` array of tables with ``code`` keys.""" + """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 isinstance(value[0], dict) - and "code" in 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} + 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()) diff --git a/src/agents/gds-agent-game-designer/SKILL.md b/src/agents/gds-agent-game-designer/SKILL.md index 38c802e..15b1807 100644 --- a/src/agents/gds-agent-game-designer/SKILL.md +++ b/src/agents/gds-agent-game-designer/SKILL.md @@ -24,8 +24,6 @@ Use the JSON output as resolved values. 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. -4. **Inject after** -- If `inject.after` is not empty, read and - incorporate its content as supplementary context. 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. diff --git a/src/agents/gds-agent-game-designer/customize.toml b/src/agents/gds-agent-game-designer/customize.toml index bc058e3..d519265 100644 --- a/src/agents/gds-agent-game-designer/customize.toml +++ b/src/agents/gds-agent-game-designer/customize.toml @@ -53,4 +53,3 @@ Design what players want to FEEL, not what they say they want. Prototype fast - # ────────────────────────────────────────────────────────────────── [inject] before = "" -after = "" diff --git a/src/agents/gds-agent-game-designer/scripts/resolve-customization.py b/src/agents/gds-agent-game-designer/scripts/resolve-customization.py index 6bbce66..c102b15 100755 --- a/src/agents/gds-agent-game-designer/scripts/resolve-customization.py +++ b/src/agents/gds-agent-game-designer/scripts/resolve-customization.py @@ -21,7 +21,6 @@ import argparse import json -import os import sys import tomllib from pathlib import Path @@ -57,19 +56,21 @@ def load_toml(path: Path) -> dict[str, Any]: # --------------------------------------------------------------------------- def _is_menu_array(value: Any) -> bool: - """True when *value* looks like a ``[[menu]]`` array of tables with ``code`` keys.""" + """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 isinstance(value[0], dict) - and "code" in 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} + 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()) diff --git a/src/agents/gds-agent-game-dev/SKILL.md b/src/agents/gds-agent-game-dev/SKILL.md index 7cc4891..729b0c3 100644 --- a/src/agents/gds-agent-game-dev/SKILL.md +++ b/src/agents/gds-agent-game-dev/SKILL.md @@ -24,8 +24,6 @@ Use the JSON output as resolved values. 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. -4. **Inject after** -- If `inject.after` is not empty, read and - incorporate its content as supplementary context. 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. diff --git a/src/agents/gds-agent-game-dev/customize.toml b/src/agents/gds-agent-game-dev/customize.toml index cf09306..8fbb5a2 100644 --- a/src/agents/gds-agent-game-dev/customize.toml +++ b/src/agents/gds-agent-game-dev/customize.toml @@ -53,4 +53,3 @@ principles = """\ # ────────────────────────────────────────────────────────────────── [inject] before = "" -after = "" diff --git a/src/agents/gds-agent-game-dev/scripts/resolve-customization.py b/src/agents/gds-agent-game-dev/scripts/resolve-customization.py index 6bbce66..c102b15 100755 --- a/src/agents/gds-agent-game-dev/scripts/resolve-customization.py +++ b/src/agents/gds-agent-game-dev/scripts/resolve-customization.py @@ -21,7 +21,6 @@ import argparse import json -import os import sys import tomllib from pathlib import Path @@ -57,19 +56,21 @@ def load_toml(path: Path) -> dict[str, Any]: # --------------------------------------------------------------------------- def _is_menu_array(value: Any) -> bool: - """True when *value* looks like a ``[[menu]]`` array of tables with ``code`` keys.""" + """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 isinstance(value[0], dict) - and "code" in 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} + 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()) diff --git a/src/agents/gds-agent-game-qa/SKILL.md b/src/agents/gds-agent-game-qa/SKILL.md index 72a9961..7b794fa 100644 --- a/src/agents/gds-agent-game-qa/SKILL.md +++ b/src/agents/gds-agent-game-qa/SKILL.md @@ -24,8 +24,6 @@ Use the JSON output as resolved values. 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. -4. **Inject after** -- If `inject.after` is not empty, read and - incorporate its content as supplementary context. 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. diff --git a/src/agents/gds-agent-game-qa/customize.toml b/src/agents/gds-agent-game-qa/customize.toml index 309deb2..9958fed 100644 --- a/src/agents/gds-agent-game-qa/customize.toml +++ b/src/agents/gds-agent-game-qa/customize.toml @@ -53,4 +53,3 @@ Test what matters: gameplay feel, performance, progression. Automated tests catc # ────────────────────────────────────────────────────────────────── [inject] before = "" -after = "" diff --git a/src/agents/gds-agent-game-qa/scripts/resolve-customization.py b/src/agents/gds-agent-game-qa/scripts/resolve-customization.py index 6bbce66..c102b15 100755 --- a/src/agents/gds-agent-game-qa/scripts/resolve-customization.py +++ b/src/agents/gds-agent-game-qa/scripts/resolve-customization.py @@ -21,7 +21,6 @@ import argparse import json -import os import sys import tomllib from pathlib import Path @@ -57,19 +56,21 @@ def load_toml(path: Path) -> dict[str, Any]: # --------------------------------------------------------------------------- def _is_menu_array(value: Any) -> bool: - """True when *value* looks like a ``[[menu]]`` array of tables with ``code`` keys.""" + """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 isinstance(value[0], dict) - and "code" in 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} + 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()) diff --git a/src/agents/gds-agent-game-scrum-master/SKILL.md b/src/agents/gds-agent-game-scrum-master/SKILL.md index 8eb2218..f2277de 100644 --- a/src/agents/gds-agent-game-scrum-master/SKILL.md +++ b/src/agents/gds-agent-game-scrum-master/SKILL.md @@ -24,8 +24,6 @@ Use the JSON output as resolved values. 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. -4. **Inject after** -- If `inject.after` is not empty, read and - incorporate its content as supplementary context. 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. diff --git a/src/agents/gds-agent-game-scrum-master/customize.toml b/src/agents/gds-agent-game-scrum-master/customize.toml index cb5cbaf..06a0762 100644 --- a/src/agents/gds-agent-game-scrum-master/customize.toml +++ b/src/agents/gds-agent-game-scrum-master/customize.toml @@ -53,4 +53,3 @@ Every sprint delivers playable increments. Clean separation between design and i # ────────────────────────────────────────────────────────────────── [inject] before = "" -after = "" diff --git a/src/agents/gds-agent-game-scrum-master/scripts/resolve-customization.py b/src/agents/gds-agent-game-scrum-master/scripts/resolve-customization.py index 6bbce66..c102b15 100755 --- a/src/agents/gds-agent-game-scrum-master/scripts/resolve-customization.py +++ b/src/agents/gds-agent-game-scrum-master/scripts/resolve-customization.py @@ -21,7 +21,6 @@ import argparse import json -import os import sys import tomllib from pathlib import Path @@ -57,19 +56,21 @@ def load_toml(path: Path) -> dict[str, Any]: # --------------------------------------------------------------------------- def _is_menu_array(value: Any) -> bool: - """True when *value* looks like a ``[[menu]]`` array of tables with ``code`` keys.""" + """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 isinstance(value[0], dict) - and "code" in 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} + 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()) diff --git a/src/agents/gds-agent-game-solo-dev/SKILL.md b/src/agents/gds-agent-game-solo-dev/SKILL.md index 8a0ce3f..27c5492 100644 --- a/src/agents/gds-agent-game-solo-dev/SKILL.md +++ b/src/agents/gds-agent-game-solo-dev/SKILL.md @@ -24,8 +24,6 @@ Use the JSON output as resolved values. 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. -4. **Inject after** -- If `inject.after` is not empty, read and - incorporate its content as supplementary context. 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. diff --git a/src/agents/gds-agent-game-solo-dev/customize.toml b/src/agents/gds-agent-game-solo-dev/customize.toml index 7b8b3e1..5f2ac66 100644 --- a/src/agents/gds-agent-game-solo-dev/customize.toml +++ b/src/agents/gds-agent-game-solo-dev/customize.toml @@ -53,4 +53,3 @@ Prototype fast, fail fast, iterate faster. Quick Flow is the indie way. A playab # ────────────────────────────────────────────────────────────────── [inject] before = "" -after = "" diff --git a/src/agents/gds-agent-game-solo-dev/scripts/resolve-customization.py b/src/agents/gds-agent-game-solo-dev/scripts/resolve-customization.py index 6bbce66..c102b15 100755 --- a/src/agents/gds-agent-game-solo-dev/scripts/resolve-customization.py +++ b/src/agents/gds-agent-game-solo-dev/scripts/resolve-customization.py @@ -21,7 +21,6 @@ import argparse import json -import os import sys import tomllib from pathlib import Path @@ -57,19 +56,21 @@ def load_toml(path: Path) -> dict[str, Any]: # --------------------------------------------------------------------------- def _is_menu_array(value: Any) -> bool: - """True when *value* looks like a ``[[menu]]`` array of tables with ``code`` keys.""" + """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 isinstance(value[0], dict) - and "code" in 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} + 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()) diff --git a/src/agents/gds-agent-tech-writer/SKILL.md b/src/agents/gds-agent-tech-writer/SKILL.md index 17e90b7..96b87e6 100644 --- a/src/agents/gds-agent-tech-writer/SKILL.md +++ b/src/agents/gds-agent-tech-writer/SKILL.md @@ -24,8 +24,6 @@ Use the JSON output as resolved values. 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. -4. **Inject after** -- If `inject.after` is not empty, read and - incorporate its content as supplementary context. 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. diff --git a/src/agents/gds-agent-tech-writer/customize.toml b/src/agents/gds-agent-tech-writer/customize.toml index cc73104..ad7c2b7 100644 --- a/src/agents/gds-agent-tech-writer/customize.toml +++ b/src/agents/gds-agent-tech-writer/customize.toml @@ -53,4 +53,3 @@ Every Technical Document I touch helps someone accomplish a task. Clarity above # ────────────────────────────────────────────────────────────────── [inject] before = "" -after = "" diff --git a/src/agents/gds-agent-tech-writer/scripts/resolve-customization.py b/src/agents/gds-agent-tech-writer/scripts/resolve-customization.py index 6bbce66..c102b15 100755 --- a/src/agents/gds-agent-tech-writer/scripts/resolve-customization.py +++ b/src/agents/gds-agent-tech-writer/scripts/resolve-customization.py @@ -21,7 +21,6 @@ import argparse import json -import os import sys import tomllib from pathlib import Path @@ -57,19 +56,21 @@ def load_toml(path: Path) -> dict[str, Any]: # --------------------------------------------------------------------------- def _is_menu_array(value: Any) -> bool: - """True when *value* looks like a ``[[menu]]`` array of tables with ``code`` keys.""" + """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 isinstance(value[0], dict) - and "code" in 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} + 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()) diff --git a/src/workflows/1-preproduction/gds-brainstorm-game/scripts/resolve-customization.py b/src/workflows/1-preproduction/gds-brainstorm-game/scripts/resolve-customization.py index 6bbce66..c102b15 100755 --- a/src/workflows/1-preproduction/gds-brainstorm-game/scripts/resolve-customization.py +++ b/src/workflows/1-preproduction/gds-brainstorm-game/scripts/resolve-customization.py @@ -21,7 +21,6 @@ import argparse import json -import os import sys import tomllib from pathlib import Path @@ -57,19 +56,21 @@ def load_toml(path: Path) -> dict[str, Any]: # --------------------------------------------------------------------------- def _is_menu_array(value: Any) -> bool: - """True when *value* looks like a ``[[menu]]`` array of tables with ``code`` keys.""" + """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 isinstance(value[0], dict) - and "code" in 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} + 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()) diff --git a/src/workflows/1-preproduction/gds-create-game-brief/scripts/resolve-customization.py b/src/workflows/1-preproduction/gds-create-game-brief/scripts/resolve-customization.py index 6bbce66..c102b15 100755 --- a/src/workflows/1-preproduction/gds-create-game-brief/scripts/resolve-customization.py +++ b/src/workflows/1-preproduction/gds-create-game-brief/scripts/resolve-customization.py @@ -21,7 +21,6 @@ import argparse import json -import os import sys import tomllib from pathlib import Path @@ -57,19 +56,21 @@ def load_toml(path: Path) -> dict[str, Any]: # --------------------------------------------------------------------------- def _is_menu_array(value: Any) -> bool: - """True when *value* looks like a ``[[menu]]`` array of tables with ``code`` keys.""" + """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 isinstance(value[0], dict) - and "code" in 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} + 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()) diff --git a/src/workflows/1-preproduction/research/gds-domain-research/scripts/resolve-customization.py b/src/workflows/1-preproduction/research/gds-domain-research/scripts/resolve-customization.py index 6bbce66..c102b15 100755 --- a/src/workflows/1-preproduction/research/gds-domain-research/scripts/resolve-customization.py +++ b/src/workflows/1-preproduction/research/gds-domain-research/scripts/resolve-customization.py @@ -21,7 +21,6 @@ import argparse import json -import os import sys import tomllib from pathlib import Path @@ -57,19 +56,21 @@ def load_toml(path: Path) -> dict[str, Any]: # --------------------------------------------------------------------------- def _is_menu_array(value: Any) -> bool: - """True when *value* looks like a ``[[menu]]`` array of tables with ``code`` keys.""" + """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 isinstance(value[0], dict) - and "code" in 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} + 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()) diff --git a/src/workflows/2-design/gds-create-gdd/scripts/resolve-customization.py b/src/workflows/2-design/gds-create-gdd/scripts/resolve-customization.py index 6bbce66..c102b15 100755 --- a/src/workflows/2-design/gds-create-gdd/scripts/resolve-customization.py +++ b/src/workflows/2-design/gds-create-gdd/scripts/resolve-customization.py @@ -21,7 +21,6 @@ import argparse import json -import os import sys import tomllib from pathlib import Path @@ -57,19 +56,21 @@ def load_toml(path: Path) -> dict[str, Any]: # --------------------------------------------------------------------------- def _is_menu_array(value: Any) -> bool: - """True when *value* looks like a ``[[menu]]`` array of tables with ``code`` keys.""" + """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 isinstance(value[0], dict) - and "code" in 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} + 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()) diff --git a/src/workflows/2-design/gds-create-narrative/scripts/resolve-customization.py b/src/workflows/2-design/gds-create-narrative/scripts/resolve-customization.py index 6bbce66..c102b15 100755 --- a/src/workflows/2-design/gds-create-narrative/scripts/resolve-customization.py +++ b/src/workflows/2-design/gds-create-narrative/scripts/resolve-customization.py @@ -21,7 +21,6 @@ import argparse import json -import os import sys import tomllib from pathlib import Path @@ -57,19 +56,21 @@ def load_toml(path: Path) -> dict[str, Any]: # --------------------------------------------------------------------------- def _is_menu_array(value: Any) -> bool: - """True when *value* looks like a ``[[menu]]`` array of tables with ``code`` keys.""" + """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 isinstance(value[0], dict) - and "code" in 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} + 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()) diff --git a/src/workflows/2-design/gds-create-ux-design/scripts/resolve-customization.py b/src/workflows/2-design/gds-create-ux-design/scripts/resolve-customization.py index 6bbce66..c102b15 100755 --- a/src/workflows/2-design/gds-create-ux-design/scripts/resolve-customization.py +++ b/src/workflows/2-design/gds-create-ux-design/scripts/resolve-customization.py @@ -21,7 +21,6 @@ import argparse import json -import os import sys import tomllib from pathlib import Path @@ -57,19 +56,21 @@ def load_toml(path: Path) -> dict[str, Any]: # --------------------------------------------------------------------------- def _is_menu_array(value: Any) -> bool: - """True when *value* looks like a ``[[menu]]`` array of tables with ``code`` keys.""" + """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 isinstance(value[0], dict) - and "code" in 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} + 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()) diff --git a/src/workflows/3-technical/gds-check-implementation-readiness/scripts/resolve-customization.py b/src/workflows/3-technical/gds-check-implementation-readiness/scripts/resolve-customization.py index 6bbce66..c102b15 100755 --- a/src/workflows/3-technical/gds-check-implementation-readiness/scripts/resolve-customization.py +++ b/src/workflows/3-technical/gds-check-implementation-readiness/scripts/resolve-customization.py @@ -21,7 +21,6 @@ import argparse import json -import os import sys import tomllib from pathlib import Path @@ -57,19 +56,21 @@ def load_toml(path: Path) -> dict[str, Any]: # --------------------------------------------------------------------------- def _is_menu_array(value: Any) -> bool: - """True when *value* looks like a ``[[menu]]`` array of tables with ``code`` keys.""" + """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 isinstance(value[0], dict) - and "code" in 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} + 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()) diff --git a/src/workflows/3-technical/gds-create-epics-and-stories/scripts/resolve-customization.py b/src/workflows/3-technical/gds-create-epics-and-stories/scripts/resolve-customization.py index 6bbce66..c102b15 100755 --- a/src/workflows/3-technical/gds-create-epics-and-stories/scripts/resolve-customization.py +++ b/src/workflows/3-technical/gds-create-epics-and-stories/scripts/resolve-customization.py @@ -21,7 +21,6 @@ import argparse import json -import os import sys import tomllib from pathlib import Path @@ -57,19 +56,21 @@ def load_toml(path: Path) -> dict[str, Any]: # --------------------------------------------------------------------------- def _is_menu_array(value: Any) -> bool: - """True when *value* looks like a ``[[menu]]`` array of tables with ``code`` keys.""" + """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 isinstance(value[0], dict) - and "code" in 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} + 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()) diff --git a/src/workflows/3-technical/gds-game-architecture/scripts/resolve-customization.py b/src/workflows/3-technical/gds-game-architecture/scripts/resolve-customization.py index 6bbce66..c102b15 100755 --- a/src/workflows/3-technical/gds-game-architecture/scripts/resolve-customization.py +++ b/src/workflows/3-technical/gds-game-architecture/scripts/resolve-customization.py @@ -21,7 +21,6 @@ import argparse import json -import os import sys import tomllib from pathlib import Path @@ -57,19 +56,21 @@ def load_toml(path: Path) -> dict[str, Any]: # --------------------------------------------------------------------------- def _is_menu_array(value: Any) -> bool: - """True when *value* looks like a ``[[menu]]`` array of tables with ``code`` keys.""" + """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 isinstance(value[0], dict) - and "code" in 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} + 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()) diff --git a/src/workflows/3-technical/gds-generate-project-context/scripts/resolve-customization.py b/src/workflows/3-technical/gds-generate-project-context/scripts/resolve-customization.py index 6bbce66..c102b15 100755 --- a/src/workflows/3-technical/gds-generate-project-context/scripts/resolve-customization.py +++ b/src/workflows/3-technical/gds-generate-project-context/scripts/resolve-customization.py @@ -21,7 +21,6 @@ import argparse import json -import os import sys import tomllib from pathlib import Path @@ -57,19 +56,21 @@ def load_toml(path: Path) -> dict[str, Any]: # --------------------------------------------------------------------------- def _is_menu_array(value: Any) -> bool: - """True when *value* looks like a ``[[menu]]`` array of tables with ``code`` keys.""" + """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 isinstance(value[0], dict) - and "code" in 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} + 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()) diff --git a/src/workflows/4-production/gds-code-review/scripts/resolve-customization.py b/src/workflows/4-production/gds-code-review/scripts/resolve-customization.py index 6bbce66..c102b15 100755 --- a/src/workflows/4-production/gds-code-review/scripts/resolve-customization.py +++ b/src/workflows/4-production/gds-code-review/scripts/resolve-customization.py @@ -21,7 +21,6 @@ import argparse import json -import os import sys import tomllib from pathlib import Path @@ -57,19 +56,21 @@ def load_toml(path: Path) -> dict[str, Any]: # --------------------------------------------------------------------------- def _is_menu_array(value: Any) -> bool: - """True when *value* looks like a ``[[menu]]`` array of tables with ``code`` keys.""" + """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 isinstance(value[0], dict) - and "code" in 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} + 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()) diff --git a/src/workflows/4-production/gds-correct-course/scripts/resolve-customization.py b/src/workflows/4-production/gds-correct-course/scripts/resolve-customization.py index 6bbce66..c102b15 100755 --- a/src/workflows/4-production/gds-correct-course/scripts/resolve-customization.py +++ b/src/workflows/4-production/gds-correct-course/scripts/resolve-customization.py @@ -21,7 +21,6 @@ import argparse import json -import os import sys import tomllib from pathlib import Path @@ -57,19 +56,21 @@ def load_toml(path: Path) -> dict[str, Any]: # --------------------------------------------------------------------------- def _is_menu_array(value: Any) -> bool: - """True when *value* looks like a ``[[menu]]`` array of tables with ``code`` keys.""" + """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 isinstance(value[0], dict) - and "code" in 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} + 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()) diff --git a/src/workflows/4-production/gds-create-story/scripts/resolve-customization.py b/src/workflows/4-production/gds-create-story/scripts/resolve-customization.py index 6bbce66..c102b15 100755 --- a/src/workflows/4-production/gds-create-story/scripts/resolve-customization.py +++ b/src/workflows/4-production/gds-create-story/scripts/resolve-customization.py @@ -21,7 +21,6 @@ import argparse import json -import os import sys import tomllib from pathlib import Path @@ -57,19 +56,21 @@ def load_toml(path: Path) -> dict[str, Any]: # --------------------------------------------------------------------------- def _is_menu_array(value: Any) -> bool: - """True when *value* looks like a ``[[menu]]`` array of tables with ``code`` keys.""" + """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 isinstance(value[0], dict) - and "code" in 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} + 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()) diff --git a/src/workflows/4-production/gds-dev-story/scripts/resolve-customization.py b/src/workflows/4-production/gds-dev-story/scripts/resolve-customization.py index 6bbce66..c102b15 100755 --- a/src/workflows/4-production/gds-dev-story/scripts/resolve-customization.py +++ b/src/workflows/4-production/gds-dev-story/scripts/resolve-customization.py @@ -21,7 +21,6 @@ import argparse import json -import os import sys import tomllib from pathlib import Path @@ -57,19 +56,21 @@ def load_toml(path: Path) -> dict[str, Any]: # --------------------------------------------------------------------------- def _is_menu_array(value: Any) -> bool: - """True when *value* looks like a ``[[menu]]`` array of tables with ``code`` keys.""" + """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 isinstance(value[0], dict) - and "code" in 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} + 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()) diff --git a/src/workflows/4-production/gds-retrospective/scripts/resolve-customization.py b/src/workflows/4-production/gds-retrospective/scripts/resolve-customization.py index 6bbce66..c102b15 100755 --- a/src/workflows/4-production/gds-retrospective/scripts/resolve-customization.py +++ b/src/workflows/4-production/gds-retrospective/scripts/resolve-customization.py @@ -21,7 +21,6 @@ import argparse import json -import os import sys import tomllib from pathlib import Path @@ -57,19 +56,21 @@ def load_toml(path: Path) -> dict[str, Any]: # --------------------------------------------------------------------------- def _is_menu_array(value: Any) -> bool: - """True when *value* looks like a ``[[menu]]`` array of tables with ``code`` keys.""" + """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 isinstance(value[0], dict) - and "code" in 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} + 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()) diff --git a/src/workflows/4-production/gds-sprint-planning/scripts/resolve-customization.py b/src/workflows/4-production/gds-sprint-planning/scripts/resolve-customization.py index 6bbce66..c102b15 100755 --- a/src/workflows/4-production/gds-sprint-planning/scripts/resolve-customization.py +++ b/src/workflows/4-production/gds-sprint-planning/scripts/resolve-customization.py @@ -21,7 +21,6 @@ import argparse import json -import os import sys import tomllib from pathlib import Path @@ -57,19 +56,21 @@ def load_toml(path: Path) -> dict[str, Any]: # --------------------------------------------------------------------------- def _is_menu_array(value: Any) -> bool: - """True when *value* looks like a ``[[menu]]`` array of tables with ``code`` keys.""" + """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 isinstance(value[0], dict) - and "code" in 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} + 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()) diff --git a/src/workflows/4-production/gds-sprint-status/scripts/resolve-customization.py b/src/workflows/4-production/gds-sprint-status/scripts/resolve-customization.py index 6bbce66..c102b15 100755 --- a/src/workflows/4-production/gds-sprint-status/scripts/resolve-customization.py +++ b/src/workflows/4-production/gds-sprint-status/scripts/resolve-customization.py @@ -21,7 +21,6 @@ import argparse import json -import os import sys import tomllib from pathlib import Path @@ -57,19 +56,21 @@ def load_toml(path: Path) -> dict[str, Any]: # --------------------------------------------------------------------------- def _is_menu_array(value: Any) -> bool: - """True when *value* looks like a ``[[menu]]`` array of tables with ``code`` keys.""" + """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 isinstance(value[0], dict) - and "code" in 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} + 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()) diff --git a/src/workflows/gametest/gds-e2e-scaffold/scripts/resolve-customization.py b/src/workflows/gametest/gds-e2e-scaffold/scripts/resolve-customization.py index 6bbce66..c102b15 100755 --- a/src/workflows/gametest/gds-e2e-scaffold/scripts/resolve-customization.py +++ b/src/workflows/gametest/gds-e2e-scaffold/scripts/resolve-customization.py @@ -21,7 +21,6 @@ import argparse import json -import os import sys import tomllib from pathlib import Path @@ -57,19 +56,21 @@ def load_toml(path: Path) -> dict[str, Any]: # --------------------------------------------------------------------------- def _is_menu_array(value: Any) -> bool: - """True when *value* looks like a ``[[menu]]`` array of tables with ``code`` keys.""" + """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 isinstance(value[0], dict) - and "code" in 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} + 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()) diff --git a/src/workflows/gametest/gds-performance-test/scripts/resolve-customization.py b/src/workflows/gametest/gds-performance-test/scripts/resolve-customization.py index 6bbce66..c102b15 100755 --- a/src/workflows/gametest/gds-performance-test/scripts/resolve-customization.py +++ b/src/workflows/gametest/gds-performance-test/scripts/resolve-customization.py @@ -21,7 +21,6 @@ import argparse import json -import os import sys import tomllib from pathlib import Path @@ -57,19 +56,21 @@ def load_toml(path: Path) -> dict[str, Any]: # --------------------------------------------------------------------------- def _is_menu_array(value: Any) -> bool: - """True when *value* looks like a ``[[menu]]`` array of tables with ``code`` keys.""" + """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 isinstance(value[0], dict) - and "code" in 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} + 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()) diff --git a/src/workflows/gametest/gds-playtest-plan/scripts/resolve-customization.py b/src/workflows/gametest/gds-playtest-plan/scripts/resolve-customization.py index 6bbce66..c102b15 100755 --- a/src/workflows/gametest/gds-playtest-plan/scripts/resolve-customization.py +++ b/src/workflows/gametest/gds-playtest-plan/scripts/resolve-customization.py @@ -21,7 +21,6 @@ import argparse import json -import os import sys import tomllib from pathlib import Path @@ -57,19 +56,21 @@ def load_toml(path: Path) -> dict[str, Any]: # --------------------------------------------------------------------------- def _is_menu_array(value: Any) -> bool: - """True when *value* looks like a ``[[menu]]`` array of tables with ``code`` keys.""" + """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 isinstance(value[0], dict) - and "code" in 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} + 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()) diff --git a/src/workflows/gametest/gds-test-automate/scripts/resolve-customization.py b/src/workflows/gametest/gds-test-automate/scripts/resolve-customization.py index 6bbce66..c102b15 100755 --- a/src/workflows/gametest/gds-test-automate/scripts/resolve-customization.py +++ b/src/workflows/gametest/gds-test-automate/scripts/resolve-customization.py @@ -21,7 +21,6 @@ import argparse import json -import os import sys import tomllib from pathlib import Path @@ -57,19 +56,21 @@ def load_toml(path: Path) -> dict[str, Any]: # --------------------------------------------------------------------------- def _is_menu_array(value: Any) -> bool: - """True when *value* looks like a ``[[menu]]`` array of tables with ``code`` keys.""" + """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 isinstance(value[0], dict) - and "code" in 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} + 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()) diff --git a/src/workflows/gametest/gds-test-design/scripts/resolve-customization.py b/src/workflows/gametest/gds-test-design/scripts/resolve-customization.py index 6bbce66..c102b15 100755 --- a/src/workflows/gametest/gds-test-design/scripts/resolve-customization.py +++ b/src/workflows/gametest/gds-test-design/scripts/resolve-customization.py @@ -21,7 +21,6 @@ import argparse import json -import os import sys import tomllib from pathlib import Path @@ -57,19 +56,21 @@ def load_toml(path: Path) -> dict[str, Any]: # --------------------------------------------------------------------------- def _is_menu_array(value: Any) -> bool: - """True when *value* looks like a ``[[menu]]`` array of tables with ``code`` keys.""" + """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 isinstance(value[0], dict) - and "code" in 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} + 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()) diff --git a/src/workflows/gametest/gds-test-framework/scripts/resolve-customization.py b/src/workflows/gametest/gds-test-framework/scripts/resolve-customization.py index 6bbce66..c102b15 100755 --- a/src/workflows/gametest/gds-test-framework/scripts/resolve-customization.py +++ b/src/workflows/gametest/gds-test-framework/scripts/resolve-customization.py @@ -21,7 +21,6 @@ import argparse import json -import os import sys import tomllib from pathlib import Path @@ -57,19 +56,21 @@ def load_toml(path: Path) -> dict[str, Any]: # --------------------------------------------------------------------------- def _is_menu_array(value: Any) -> bool: - """True when *value* looks like a ``[[menu]]`` array of tables with ``code`` keys.""" + """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 isinstance(value[0], dict) - and "code" in 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} + 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()) diff --git a/src/workflows/gametest/gds-test-review/scripts/resolve-customization.py b/src/workflows/gametest/gds-test-review/scripts/resolve-customization.py index 6bbce66..c102b15 100755 --- a/src/workflows/gametest/gds-test-review/scripts/resolve-customization.py +++ b/src/workflows/gametest/gds-test-review/scripts/resolve-customization.py @@ -21,7 +21,6 @@ import argparse import json -import os import sys import tomllib from pathlib import Path @@ -57,19 +56,21 @@ def load_toml(path: Path) -> dict[str, Any]: # --------------------------------------------------------------------------- def _is_menu_array(value: Any) -> bool: - """True when *value* looks like a ``[[menu]]`` array of tables with ``code`` keys.""" + """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 isinstance(value[0], dict) - and "code" in 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} + 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()) diff --git a/src/workflows/gds-document-project/scripts/resolve-customization.py b/src/workflows/gds-document-project/scripts/resolve-customization.py index 6bbce66..c102b15 100755 --- a/src/workflows/gds-document-project/scripts/resolve-customization.py +++ b/src/workflows/gds-document-project/scripts/resolve-customization.py @@ -21,7 +21,6 @@ import argparse import json -import os import sys import tomllib from pathlib import Path @@ -57,19 +56,21 @@ def load_toml(path: Path) -> dict[str, Any]: # --------------------------------------------------------------------------- def _is_menu_array(value: Any) -> bool: - """True when *value* looks like a ``[[menu]]`` array of tables with ``code`` keys.""" + """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 isinstance(value[0], dict) - and "code" in 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} + 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()) diff --git a/src/workflows/gds-quick-flow/gds-quick-dev-new-preview/scripts/resolve-customization.py b/src/workflows/gds-quick-flow/gds-quick-dev-new-preview/scripts/resolve-customization.py index 6bbce66..c102b15 100755 --- a/src/workflows/gds-quick-flow/gds-quick-dev-new-preview/scripts/resolve-customization.py +++ b/src/workflows/gds-quick-flow/gds-quick-dev-new-preview/scripts/resolve-customization.py @@ -21,7 +21,6 @@ import argparse import json -import os import sys import tomllib from pathlib import Path @@ -57,19 +56,21 @@ def load_toml(path: Path) -> dict[str, Any]: # --------------------------------------------------------------------------- def _is_menu_array(value: Any) -> bool: - """True when *value* looks like a ``[[menu]]`` array of tables with ``code`` keys.""" + """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 isinstance(value[0], dict) - and "code" in 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} + 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()) diff --git a/src/workflows/gds-quick-flow/gds-quick-dev/scripts/resolve-customization.py b/src/workflows/gds-quick-flow/gds-quick-dev/scripts/resolve-customization.py index 6bbce66..c102b15 100755 --- a/src/workflows/gds-quick-flow/gds-quick-dev/scripts/resolve-customization.py +++ b/src/workflows/gds-quick-flow/gds-quick-dev/scripts/resolve-customization.py @@ -21,7 +21,6 @@ import argparse import json -import os import sys import tomllib from pathlib import Path @@ -57,19 +56,21 @@ def load_toml(path: Path) -> dict[str, Any]: # --------------------------------------------------------------------------- def _is_menu_array(value: Any) -> bool: - """True when *value* looks like a ``[[menu]]`` array of tables with ``code`` keys.""" + """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 isinstance(value[0], dict) - and "code" in 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} + 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()) diff --git a/src/workflows/gds-quick-flow/gds-quick-spec/scripts/resolve-customization.py b/src/workflows/gds-quick-flow/gds-quick-spec/scripts/resolve-customization.py index 6bbce66..c102b15 100755 --- a/src/workflows/gds-quick-flow/gds-quick-spec/scripts/resolve-customization.py +++ b/src/workflows/gds-quick-flow/gds-quick-spec/scripts/resolve-customization.py @@ -21,7 +21,6 @@ import argparse import json -import os import sys import tomllib from pathlib import Path @@ -57,19 +56,21 @@ def load_toml(path: Path) -> dict[str, Any]: # --------------------------------------------------------------------------- def _is_menu_array(value: Any) -> bool: - """True when *value* looks like a ``[[menu]]`` array of tables with ``code`` keys.""" + """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 isinstance(value[0], dict) - and "code" in 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} + 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()) From df47f5307b58762b3d5cdc864725fd7bb1c9a698 Mon Sep 17 00:00:00 2001 From: Brian Madison Date: Tue, 14 Apr 2026 16:10:38 -0500 Subject: [PATCH 6/7] fix: harden resolve script type hint + improve inject prompt wording - Add type: ignore[arg-type] to merge_menu call (Pylance narrowing limitation) - Reword inject.before in workflows: "prepend to active instructions and follow it" - Reword inject.after in workflows: "append to active instructions and follow it" - Make additional_resources lazy: note list but don't eagerly load --- .../scripts/resolve-customization.py | 2 +- .../scripts/resolve-customization.py | 2 +- .../gds-agent-game-dev/scripts/resolve-customization.py | 2 +- .../gds-agent-game-qa/scripts/resolve-customization.py | 2 +- .../scripts/resolve-customization.py | 2 +- .../scripts/resolve-customization.py | 2 +- .../gds-agent-tech-writer/scripts/resolve-customization.py | 2 +- src/workflows/1-preproduction/gds-brainstorm-game/SKILL.md | 6 +++--- .../gds-brainstorm-game/scripts/resolve-customization.py | 2 +- .../1-preproduction/gds-create-game-brief/SKILL.md | 6 +++--- .../gds-create-game-brief/scripts/resolve-customization.py | 2 +- .../1-preproduction/research/gds-domain-research/SKILL.md | 6 +++--- .../gds-domain-research/scripts/resolve-customization.py | 2 +- src/workflows/2-design/gds-create-gdd/SKILL.md | 6 +++--- .../gds-create-gdd/scripts/resolve-customization.py | 2 +- src/workflows/2-design/gds-create-narrative/SKILL.md | 6 +++--- .../gds-create-narrative/scripts/resolve-customization.py | 2 +- src/workflows/2-design/gds-create-ux-design/SKILL.md | 6 +++--- .../gds-create-ux-design/scripts/resolve-customization.py | 2 +- .../3-technical/gds-check-implementation-readiness/SKILL.md | 6 +++--- .../scripts/resolve-customization.py | 2 +- .../3-technical/gds-create-epics-and-stories/SKILL.md | 6 +++--- .../scripts/resolve-customization.py | 2 +- src/workflows/3-technical/gds-game-architecture/SKILL.md | 6 +++--- .../gds-game-architecture/scripts/resolve-customization.py | 2 +- .../3-technical/gds-generate-project-context/SKILL.md | 6 +++--- .../scripts/resolve-customization.py | 2 +- src/workflows/4-production/gds-code-review/SKILL.md | 6 +++--- .../gds-code-review/scripts/resolve-customization.py | 2 +- src/workflows/4-production/gds-correct-course/SKILL.md | 6 +++--- .../gds-correct-course/scripts/resolve-customization.py | 2 +- src/workflows/4-production/gds-create-story/SKILL.md | 6 +++--- .../gds-create-story/scripts/resolve-customization.py | 2 +- src/workflows/4-production/gds-dev-story/SKILL.md | 6 +++--- .../gds-dev-story/scripts/resolve-customization.py | 2 +- src/workflows/4-production/gds-retrospective/SKILL.md | 6 +++--- .../gds-retrospective/scripts/resolve-customization.py | 2 +- src/workflows/4-production/gds-sprint-planning/SKILL.md | 6 +++--- .../gds-sprint-planning/scripts/resolve-customization.py | 2 +- src/workflows/4-production/gds-sprint-status/SKILL.md | 6 +++--- .../gds-sprint-status/scripts/resolve-customization.py | 2 +- src/workflows/gametest/gds-e2e-scaffold/SKILL.md | 6 +++--- .../gds-e2e-scaffold/scripts/resolve-customization.py | 2 +- src/workflows/gametest/gds-performance-test/SKILL.md | 6 +++--- .../gds-performance-test/scripts/resolve-customization.py | 2 +- src/workflows/gametest/gds-playtest-plan/SKILL.md | 6 +++--- .../gds-playtest-plan/scripts/resolve-customization.py | 2 +- src/workflows/gametest/gds-test-automate/SKILL.md | 6 +++--- .../gds-test-automate/scripts/resolve-customization.py | 2 +- src/workflows/gametest/gds-test-design/SKILL.md | 6 +++--- .../gds-test-design/scripts/resolve-customization.py | 2 +- src/workflows/gametest/gds-test-framework/SKILL.md | 6 +++--- .../gds-test-framework/scripts/resolve-customization.py | 2 +- src/workflows/gametest/gds-test-review/SKILL.md | 6 +++--- .../gds-test-review/scripts/resolve-customization.py | 2 +- src/workflows/gds-document-project/SKILL.md | 6 +++--- .../gds-document-project/scripts/resolve-customization.py | 2 +- .../gds-quick-flow/gds-quick-dev-new-preview/SKILL.md | 6 +++--- .../scripts/resolve-customization.py | 2 +- src/workflows/gds-quick-flow/gds-quick-dev/SKILL.md | 6 +++--- .../gds-quick-dev/scripts/resolve-customization.py | 2 +- src/workflows/gds-quick-flow/gds-quick-spec/SKILL.md | 6 +++--- .../gds-quick-spec/scripts/resolve-customization.py | 2 +- 63 files changed, 119 insertions(+), 119 deletions(-) diff --git a/src/agents/gds-agent-game-architect/scripts/resolve-customization.py b/src/agents/gds-agent-game-architect/scripts/resolve-customization.py index c102b15..d9994a5 100755 --- a/src/agents/gds-agent-game-architect/scripts/resolve-customization.py +++ b/src/agents/gds-agent-game-architect/scripts/resolve-customization.py @@ -91,7 +91,7 @@ def deep_merge(base: dict[str, Any], override: dict[str, Any]) -> dict[str, Any] 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) + merged[key] = merge_menu(base_val, over_val) # type: ignore[arg-type] else: merged[key] = over_val diff --git a/src/agents/gds-agent-game-designer/scripts/resolve-customization.py b/src/agents/gds-agent-game-designer/scripts/resolve-customization.py index c102b15..d9994a5 100755 --- a/src/agents/gds-agent-game-designer/scripts/resolve-customization.py +++ b/src/agents/gds-agent-game-designer/scripts/resolve-customization.py @@ -91,7 +91,7 @@ def deep_merge(base: dict[str, Any], override: dict[str, Any]) -> dict[str, Any] 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) + merged[key] = merge_menu(base_val, over_val) # type: ignore[arg-type] else: merged[key] = over_val diff --git a/src/agents/gds-agent-game-dev/scripts/resolve-customization.py b/src/agents/gds-agent-game-dev/scripts/resolve-customization.py index c102b15..d9994a5 100755 --- a/src/agents/gds-agent-game-dev/scripts/resolve-customization.py +++ b/src/agents/gds-agent-game-dev/scripts/resolve-customization.py @@ -91,7 +91,7 @@ def deep_merge(base: dict[str, Any], override: dict[str, Any]) -> dict[str, Any] 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) + merged[key] = merge_menu(base_val, over_val) # type: ignore[arg-type] else: merged[key] = over_val diff --git a/src/agents/gds-agent-game-qa/scripts/resolve-customization.py b/src/agents/gds-agent-game-qa/scripts/resolve-customization.py index c102b15..d9994a5 100755 --- a/src/agents/gds-agent-game-qa/scripts/resolve-customization.py +++ b/src/agents/gds-agent-game-qa/scripts/resolve-customization.py @@ -91,7 +91,7 @@ def deep_merge(base: dict[str, Any], override: dict[str, Any]) -> dict[str, Any] 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) + merged[key] = merge_menu(base_val, over_val) # type: ignore[arg-type] else: merged[key] = over_val diff --git a/src/agents/gds-agent-game-scrum-master/scripts/resolve-customization.py b/src/agents/gds-agent-game-scrum-master/scripts/resolve-customization.py index c102b15..d9994a5 100755 --- a/src/agents/gds-agent-game-scrum-master/scripts/resolve-customization.py +++ b/src/agents/gds-agent-game-scrum-master/scripts/resolve-customization.py @@ -91,7 +91,7 @@ def deep_merge(base: dict[str, Any], override: dict[str, Any]) -> dict[str, Any] 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) + merged[key] = merge_menu(base_val, over_val) # type: ignore[arg-type] else: merged[key] = over_val diff --git a/src/agents/gds-agent-game-solo-dev/scripts/resolve-customization.py b/src/agents/gds-agent-game-solo-dev/scripts/resolve-customization.py index c102b15..d9994a5 100755 --- a/src/agents/gds-agent-game-solo-dev/scripts/resolve-customization.py +++ b/src/agents/gds-agent-game-solo-dev/scripts/resolve-customization.py @@ -91,7 +91,7 @@ def deep_merge(base: dict[str, Any], override: dict[str, Any]) -> dict[str, Any] 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) + merged[key] = merge_menu(base_val, over_val) # type: ignore[arg-type] else: merged[key] = over_val diff --git a/src/agents/gds-agent-tech-writer/scripts/resolve-customization.py b/src/agents/gds-agent-tech-writer/scripts/resolve-customization.py index c102b15..d9994a5 100755 --- a/src/agents/gds-agent-tech-writer/scripts/resolve-customization.py +++ b/src/agents/gds-agent-tech-writer/scripts/resolve-customization.py @@ -91,7 +91,7 @@ def deep_merge(base: dict[str, Any], override: dict[str, Any]) -> dict[str, Any] 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) + merged[key] = merge_menu(base_val, over_val) # type: ignore[arg-type] else: merged[key] = over_val diff --git a/src/workflows/1-preproduction/gds-brainstorm-game/SKILL.md b/src/workflows/1-preproduction/gds-brainstorm-game/SKILL.md index 4dff7db..fb69fa2 100644 --- a/src/workflows/1-preproduction/gds-brainstorm-game/SKILL.md +++ b/src/workflows/1-preproduction/gds-brainstorm-game/SKILL.md @@ -13,8 +13,8 @@ Resolve `inject` and `additional_resources` from customization: Run: `python3 scripts/resolve-customization.py gds-brainstorm-game --key inject --key additional_resources` Use the JSON output as resolved values. -If `inject.before` is not empty, incorporate its content as high-priority context. -If `additional_resources` is not empty, read each listed file and incorporate as reference context. +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. @@ -23,4 +23,4 @@ Follow the instructions in ./workflow.md. After the workflow completes, resolve `inject.after` from customization: Run: `python3 scripts/resolve-customization.py gds-brainstorm-game --key inject.after` -If resolved `inject.after` is not empty, incorporate its content as a final checklist or validation gate. +If resolved `inject.after` is not empty, append it to your active instructions and follow it. diff --git a/src/workflows/1-preproduction/gds-brainstorm-game/scripts/resolve-customization.py b/src/workflows/1-preproduction/gds-brainstorm-game/scripts/resolve-customization.py index c102b15..d9994a5 100755 --- a/src/workflows/1-preproduction/gds-brainstorm-game/scripts/resolve-customization.py +++ b/src/workflows/1-preproduction/gds-brainstorm-game/scripts/resolve-customization.py @@ -91,7 +91,7 @@ def deep_merge(base: dict[str, Any], override: dict[str, Any]) -> dict[str, Any] 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) + merged[key] = merge_menu(base_val, over_val) # type: ignore[arg-type] else: merged[key] = over_val diff --git a/src/workflows/1-preproduction/gds-create-game-brief/SKILL.md b/src/workflows/1-preproduction/gds-create-game-brief/SKILL.md index ce990de..06eeb2d 100644 --- a/src/workflows/1-preproduction/gds-create-game-brief/SKILL.md +++ b/src/workflows/1-preproduction/gds-create-game-brief/SKILL.md @@ -13,8 +13,8 @@ Resolve `inject` and `additional_resources` from customization: Run: `python3 scripts/resolve-customization.py gds-create-game-brief --key inject --key additional_resources` Use the JSON output as resolved values. -If `inject.before` is not empty, incorporate its content as high-priority context. -If `additional_resources` is not empty, read each listed file and incorporate as reference context. +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. @@ -23,4 +23,4 @@ Follow the instructions in ./workflow.md. After the workflow completes, resolve `inject.after` from customization: Run: `python3 scripts/resolve-customization.py gds-create-game-brief --key inject.after` -If resolved `inject.after` is not empty, incorporate its content as a final checklist or validation gate. +If resolved `inject.after` is not empty, append it to your active instructions and follow it. diff --git a/src/workflows/1-preproduction/gds-create-game-brief/scripts/resolve-customization.py b/src/workflows/1-preproduction/gds-create-game-brief/scripts/resolve-customization.py index c102b15..d9994a5 100755 --- a/src/workflows/1-preproduction/gds-create-game-brief/scripts/resolve-customization.py +++ b/src/workflows/1-preproduction/gds-create-game-brief/scripts/resolve-customization.py @@ -91,7 +91,7 @@ def deep_merge(base: dict[str, Any], override: dict[str, Any]) -> dict[str, Any] 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) + merged[key] = merge_menu(base_val, over_val) # type: ignore[arg-type] else: merged[key] = over_val diff --git a/src/workflows/1-preproduction/research/gds-domain-research/SKILL.md b/src/workflows/1-preproduction/research/gds-domain-research/SKILL.md index 7b36216..2400c22 100644 --- a/src/workflows/1-preproduction/research/gds-domain-research/SKILL.md +++ b/src/workflows/1-preproduction/research/gds-domain-research/SKILL.md @@ -13,8 +13,8 @@ Resolve `inject` and `additional_resources` from customization: Run: `python3 scripts/resolve-customization.py gds-domain-research --key inject --key additional_resources` Use the JSON output as resolved values. -If `inject.before` is not empty, incorporate its content as high-priority context. -If `additional_resources` is not empty, read each listed file and incorporate as reference context. +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. @@ -23,4 +23,4 @@ Follow the instructions in ./workflow.md. After the workflow completes, resolve `inject.after` from customization: Run: `python3 scripts/resolve-customization.py gds-domain-research --key inject.after` -If resolved `inject.after` is not empty, incorporate its content as a final checklist or validation gate. +If resolved `inject.after` is not empty, append it to your active instructions and follow it. diff --git a/src/workflows/1-preproduction/research/gds-domain-research/scripts/resolve-customization.py b/src/workflows/1-preproduction/research/gds-domain-research/scripts/resolve-customization.py index c102b15..d9994a5 100755 --- a/src/workflows/1-preproduction/research/gds-domain-research/scripts/resolve-customization.py +++ b/src/workflows/1-preproduction/research/gds-domain-research/scripts/resolve-customization.py @@ -91,7 +91,7 @@ def deep_merge(base: dict[str, Any], override: dict[str, Any]) -> dict[str, Any] 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) + merged[key] = merge_menu(base_val, over_val) # type: ignore[arg-type] else: merged[key] = over_val diff --git a/src/workflows/2-design/gds-create-gdd/SKILL.md b/src/workflows/2-design/gds-create-gdd/SKILL.md index 9767163..1dffadd 100644 --- a/src/workflows/2-design/gds-create-gdd/SKILL.md +++ b/src/workflows/2-design/gds-create-gdd/SKILL.md @@ -13,8 +13,8 @@ Resolve `inject` and `additional_resources` from customization: Run: `python3 scripts/resolve-customization.py gds-create-gdd --key inject --key additional_resources` Use the JSON output as resolved values. -If `inject.before` is not empty, incorporate its content as high-priority context. -If `additional_resources` is not empty, read each listed file and incorporate as reference context. +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. @@ -23,4 +23,4 @@ Follow the instructions in ./workflow.md. After the workflow completes, resolve `inject.after` from customization: Run: `python3 scripts/resolve-customization.py gds-create-gdd --key inject.after` -If resolved `inject.after` is not empty, incorporate its content as a final checklist or validation gate. +If resolved `inject.after` is not empty, append it to your active instructions and follow it. diff --git a/src/workflows/2-design/gds-create-gdd/scripts/resolve-customization.py b/src/workflows/2-design/gds-create-gdd/scripts/resolve-customization.py index c102b15..d9994a5 100755 --- a/src/workflows/2-design/gds-create-gdd/scripts/resolve-customization.py +++ b/src/workflows/2-design/gds-create-gdd/scripts/resolve-customization.py @@ -91,7 +91,7 @@ def deep_merge(base: dict[str, Any], override: dict[str, Any]) -> dict[str, Any] 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) + merged[key] = merge_menu(base_val, over_val) # type: ignore[arg-type] else: merged[key] = over_val diff --git a/src/workflows/2-design/gds-create-narrative/SKILL.md b/src/workflows/2-design/gds-create-narrative/SKILL.md index d2928d0..6e825a4 100644 --- a/src/workflows/2-design/gds-create-narrative/SKILL.md +++ b/src/workflows/2-design/gds-create-narrative/SKILL.md @@ -13,8 +13,8 @@ Resolve `inject` and `additional_resources` from customization: Run: `python3 scripts/resolve-customization.py gds-create-narrative --key inject --key additional_resources` Use the JSON output as resolved values. -If `inject.before` is not empty, incorporate its content as high-priority context. -If `additional_resources` is not empty, read each listed file and incorporate as reference context. +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. @@ -23,4 +23,4 @@ Follow the instructions in ./workflow.md. After the workflow completes, resolve `inject.after` from customization: Run: `python3 scripts/resolve-customization.py gds-create-narrative --key inject.after` -If resolved `inject.after` is not empty, incorporate its content as a final checklist or validation gate. +If resolved `inject.after` is not empty, append it to your active instructions and follow it. diff --git a/src/workflows/2-design/gds-create-narrative/scripts/resolve-customization.py b/src/workflows/2-design/gds-create-narrative/scripts/resolve-customization.py index c102b15..d9994a5 100755 --- a/src/workflows/2-design/gds-create-narrative/scripts/resolve-customization.py +++ b/src/workflows/2-design/gds-create-narrative/scripts/resolve-customization.py @@ -91,7 +91,7 @@ def deep_merge(base: dict[str, Any], override: dict[str, Any]) -> dict[str, Any] 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) + merged[key] = merge_menu(base_val, over_val) # type: ignore[arg-type] else: merged[key] = over_val diff --git a/src/workflows/2-design/gds-create-ux-design/SKILL.md b/src/workflows/2-design/gds-create-ux-design/SKILL.md index 618abc6..b0766d1 100644 --- a/src/workflows/2-design/gds-create-ux-design/SKILL.md +++ b/src/workflows/2-design/gds-create-ux-design/SKILL.md @@ -13,8 +13,8 @@ Resolve `inject` and `additional_resources` from customization: Run: `python3 scripts/resolve-customization.py gds-create-ux-design --key inject --key additional_resources` Use the JSON output as resolved values. -If `inject.before` is not empty, incorporate its content as high-priority context. -If `additional_resources` is not empty, read each listed file and incorporate as reference context. +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. @@ -23,4 +23,4 @@ Follow the instructions in ./workflow.md. After the workflow completes, resolve `inject.after` from customization: Run: `python3 scripts/resolve-customization.py gds-create-ux-design --key inject.after` -If resolved `inject.after` is not empty, incorporate its content as a final checklist or validation gate. +If resolved `inject.after` is not empty, append it to your active instructions and follow it. diff --git a/src/workflows/2-design/gds-create-ux-design/scripts/resolve-customization.py b/src/workflows/2-design/gds-create-ux-design/scripts/resolve-customization.py index c102b15..d9994a5 100755 --- a/src/workflows/2-design/gds-create-ux-design/scripts/resolve-customization.py +++ b/src/workflows/2-design/gds-create-ux-design/scripts/resolve-customization.py @@ -91,7 +91,7 @@ def deep_merge(base: dict[str, Any], override: dict[str, Any]) -> dict[str, Any] 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) + merged[key] = merge_menu(base_val, over_val) # type: ignore[arg-type] else: merged[key] = over_val diff --git a/src/workflows/3-technical/gds-check-implementation-readiness/SKILL.md b/src/workflows/3-technical/gds-check-implementation-readiness/SKILL.md index 129080c..d05a3b0 100644 --- a/src/workflows/3-technical/gds-check-implementation-readiness/SKILL.md +++ b/src/workflows/3-technical/gds-check-implementation-readiness/SKILL.md @@ -13,8 +13,8 @@ Resolve `inject` and `additional_resources` from customization: Run: `python3 scripts/resolve-customization.py gds-check-implementation-readiness --key inject --key additional_resources` Use the JSON output as resolved values. -If `inject.before` is not empty, incorporate its content as high-priority context. -If `additional_resources` is not empty, read each listed file and incorporate as reference context. +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. @@ -23,4 +23,4 @@ Follow the instructions in ./workflow.md. After the workflow completes, resolve `inject.after` from customization: Run: `python3 scripts/resolve-customization.py gds-check-implementation-readiness --key inject.after` -If resolved `inject.after` is not empty, incorporate its content as a final checklist or validation gate. +If resolved `inject.after` is not empty, append it to your active instructions and follow it. diff --git a/src/workflows/3-technical/gds-check-implementation-readiness/scripts/resolve-customization.py b/src/workflows/3-technical/gds-check-implementation-readiness/scripts/resolve-customization.py index c102b15..d9994a5 100755 --- a/src/workflows/3-technical/gds-check-implementation-readiness/scripts/resolve-customization.py +++ b/src/workflows/3-technical/gds-check-implementation-readiness/scripts/resolve-customization.py @@ -91,7 +91,7 @@ def deep_merge(base: dict[str, Any], override: dict[str, Any]) -> dict[str, Any] 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) + merged[key] = merge_menu(base_val, over_val) # type: ignore[arg-type] else: merged[key] = over_val diff --git a/src/workflows/3-technical/gds-create-epics-and-stories/SKILL.md b/src/workflows/3-technical/gds-create-epics-and-stories/SKILL.md index 7e4d757..274db0a 100644 --- a/src/workflows/3-technical/gds-create-epics-and-stories/SKILL.md +++ b/src/workflows/3-technical/gds-create-epics-and-stories/SKILL.md @@ -13,8 +13,8 @@ Resolve `inject` and `additional_resources` from customization: Run: `python3 scripts/resolve-customization.py gds-create-epics-and-stories --key inject --key additional_resources` Use the JSON output as resolved values. -If `inject.before` is not empty, incorporate its content as high-priority context. -If `additional_resources` is not empty, read each listed file and incorporate as reference context. +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. @@ -23,4 +23,4 @@ Follow the instructions in ./workflow.md. After the workflow completes, resolve `inject.after` from customization: Run: `python3 scripts/resolve-customization.py gds-create-epics-and-stories --key inject.after` -If resolved `inject.after` is not empty, incorporate its content as a final checklist or validation gate. +If resolved `inject.after` is not empty, append it to your active instructions and follow it. diff --git a/src/workflows/3-technical/gds-create-epics-and-stories/scripts/resolve-customization.py b/src/workflows/3-technical/gds-create-epics-and-stories/scripts/resolve-customization.py index c102b15..d9994a5 100755 --- a/src/workflows/3-technical/gds-create-epics-and-stories/scripts/resolve-customization.py +++ b/src/workflows/3-technical/gds-create-epics-and-stories/scripts/resolve-customization.py @@ -91,7 +91,7 @@ def deep_merge(base: dict[str, Any], override: dict[str, Any]) -> dict[str, Any] 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) + merged[key] = merge_menu(base_val, over_val) # type: ignore[arg-type] else: merged[key] = over_val diff --git a/src/workflows/3-technical/gds-game-architecture/SKILL.md b/src/workflows/3-technical/gds-game-architecture/SKILL.md index 93da3ce..6a611b2 100644 --- a/src/workflows/3-technical/gds-game-architecture/SKILL.md +++ b/src/workflows/3-technical/gds-game-architecture/SKILL.md @@ -13,8 +13,8 @@ Resolve `inject` and `additional_resources` from customization: Run: `python3 scripts/resolve-customization.py gds-game-architecture --key inject --key additional_resources` Use the JSON output as resolved values. -If `inject.before` is not empty, incorporate its content as high-priority context. -If `additional_resources` is not empty, read each listed file and incorporate as reference context. +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. @@ -23,4 +23,4 @@ Follow the instructions in ./workflow.md. After the workflow completes, resolve `inject.after` from customization: Run: `python3 scripts/resolve-customization.py gds-game-architecture --key inject.after` -If resolved `inject.after` is not empty, incorporate its content as a final checklist or validation gate. +If resolved `inject.after` is not empty, append it to your active instructions and follow it. diff --git a/src/workflows/3-technical/gds-game-architecture/scripts/resolve-customization.py b/src/workflows/3-technical/gds-game-architecture/scripts/resolve-customization.py index c102b15..d9994a5 100755 --- a/src/workflows/3-technical/gds-game-architecture/scripts/resolve-customization.py +++ b/src/workflows/3-technical/gds-game-architecture/scripts/resolve-customization.py @@ -91,7 +91,7 @@ def deep_merge(base: dict[str, Any], override: dict[str, Any]) -> dict[str, Any] 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) + merged[key] = merge_menu(base_val, over_val) # type: ignore[arg-type] else: merged[key] = over_val diff --git a/src/workflows/3-technical/gds-generate-project-context/SKILL.md b/src/workflows/3-technical/gds-generate-project-context/SKILL.md index 843b31a..d7be607 100644 --- a/src/workflows/3-technical/gds-generate-project-context/SKILL.md +++ b/src/workflows/3-technical/gds-generate-project-context/SKILL.md @@ -13,8 +13,8 @@ Resolve `inject` and `additional_resources` from customization: Run: `python3 scripts/resolve-customization.py gds-generate-project-context --key inject --key additional_resources` Use the JSON output as resolved values. -If `inject.before` is not empty, incorporate its content as high-priority context. -If `additional_resources` is not empty, read each listed file and incorporate as reference context. +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. @@ -23,4 +23,4 @@ Follow the instructions in ./workflow.md. After the workflow completes, resolve `inject.after` from customization: Run: `python3 scripts/resolve-customization.py gds-generate-project-context --key inject.after` -If resolved `inject.after` is not empty, incorporate its content as a final checklist or validation gate. +If resolved `inject.after` is not empty, append it to your active instructions and follow it. diff --git a/src/workflows/3-technical/gds-generate-project-context/scripts/resolve-customization.py b/src/workflows/3-technical/gds-generate-project-context/scripts/resolve-customization.py index c102b15..d9994a5 100755 --- a/src/workflows/3-technical/gds-generate-project-context/scripts/resolve-customization.py +++ b/src/workflows/3-technical/gds-generate-project-context/scripts/resolve-customization.py @@ -91,7 +91,7 @@ def deep_merge(base: dict[str, Any], override: dict[str, Any]) -> dict[str, Any] 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) + merged[key] = merge_menu(base_val, over_val) # type: ignore[arg-type] else: merged[key] = over_val diff --git a/src/workflows/4-production/gds-code-review/SKILL.md b/src/workflows/4-production/gds-code-review/SKILL.md index 76d0a4f..34c6c30 100644 --- a/src/workflows/4-production/gds-code-review/SKILL.md +++ b/src/workflows/4-production/gds-code-review/SKILL.md @@ -13,8 +13,8 @@ Resolve `inject` and `additional_resources` from customization: Run: `python3 scripts/resolve-customization.py gds-code-review --key inject --key additional_resources` Use the JSON output as resolved values. -If `inject.before` is not empty, incorporate its content as high-priority context. -If `additional_resources` is not empty, read each listed file and incorporate as reference context. +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. @@ -23,4 +23,4 @@ Follow the instructions in ./workflow.md. After the workflow completes, resolve `inject.after` from customization: Run: `python3 scripts/resolve-customization.py gds-code-review --key inject.after` -If resolved `inject.after` is not empty, incorporate its content as a final checklist or validation gate. +If resolved `inject.after` is not empty, append it to your active instructions and follow it. diff --git a/src/workflows/4-production/gds-code-review/scripts/resolve-customization.py b/src/workflows/4-production/gds-code-review/scripts/resolve-customization.py index c102b15..d9994a5 100755 --- a/src/workflows/4-production/gds-code-review/scripts/resolve-customization.py +++ b/src/workflows/4-production/gds-code-review/scripts/resolve-customization.py @@ -91,7 +91,7 @@ def deep_merge(base: dict[str, Any], override: dict[str, Any]) -> dict[str, Any] 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) + merged[key] = merge_menu(base_val, over_val) # type: ignore[arg-type] else: merged[key] = over_val diff --git a/src/workflows/4-production/gds-correct-course/SKILL.md b/src/workflows/4-production/gds-correct-course/SKILL.md index 39e7229..e686b98 100644 --- a/src/workflows/4-production/gds-correct-course/SKILL.md +++ b/src/workflows/4-production/gds-correct-course/SKILL.md @@ -13,8 +13,8 @@ Resolve `inject` and `additional_resources` from customization: Run: `python3 scripts/resolve-customization.py gds-correct-course --key inject --key additional_resources` Use the JSON output as resolved values. -If `inject.before` is not empty, incorporate its content as high-priority context. -If `additional_resources` is not empty, read each listed file and incorporate as reference context. +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. @@ -23,4 +23,4 @@ Follow the instructions in ./workflow.md. After the workflow completes, resolve `inject.after` from customization: Run: `python3 scripts/resolve-customization.py gds-correct-course --key inject.after` -If resolved `inject.after` is not empty, incorporate its content as a final checklist or validation gate. +If resolved `inject.after` is not empty, append it to your active instructions and follow it. diff --git a/src/workflows/4-production/gds-correct-course/scripts/resolve-customization.py b/src/workflows/4-production/gds-correct-course/scripts/resolve-customization.py index c102b15..d9994a5 100755 --- a/src/workflows/4-production/gds-correct-course/scripts/resolve-customization.py +++ b/src/workflows/4-production/gds-correct-course/scripts/resolve-customization.py @@ -91,7 +91,7 @@ def deep_merge(base: dict[str, Any], override: dict[str, Any]) -> dict[str, Any] 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) + merged[key] = merge_menu(base_val, over_val) # type: ignore[arg-type] else: merged[key] = over_val diff --git a/src/workflows/4-production/gds-create-story/SKILL.md b/src/workflows/4-production/gds-create-story/SKILL.md index b568978..65d9670 100644 --- a/src/workflows/4-production/gds-create-story/SKILL.md +++ b/src/workflows/4-production/gds-create-story/SKILL.md @@ -13,8 +13,8 @@ Resolve `inject` and `additional_resources` from customization: Run: `python3 scripts/resolve-customization.py gds-create-story --key inject --key additional_resources` Use the JSON output as resolved values. -If `inject.before` is not empty, incorporate its content as high-priority context. -If `additional_resources` is not empty, read each listed file and incorporate as reference context. +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. @@ -23,4 +23,4 @@ Follow the instructions in ./workflow.md. After the workflow completes, resolve `inject.after` from customization: Run: `python3 scripts/resolve-customization.py gds-create-story --key inject.after` -If resolved `inject.after` is not empty, incorporate its content as a final checklist or validation gate. +If resolved `inject.after` is not empty, append it to your active instructions and follow it. diff --git a/src/workflows/4-production/gds-create-story/scripts/resolve-customization.py b/src/workflows/4-production/gds-create-story/scripts/resolve-customization.py index c102b15..d9994a5 100755 --- a/src/workflows/4-production/gds-create-story/scripts/resolve-customization.py +++ b/src/workflows/4-production/gds-create-story/scripts/resolve-customization.py @@ -91,7 +91,7 @@ def deep_merge(base: dict[str, Any], override: dict[str, Any]) -> dict[str, Any] 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) + merged[key] = merge_menu(base_val, over_val) # type: ignore[arg-type] else: merged[key] = over_val diff --git a/src/workflows/4-production/gds-dev-story/SKILL.md b/src/workflows/4-production/gds-dev-story/SKILL.md index 5db5859..ac5a437 100644 --- a/src/workflows/4-production/gds-dev-story/SKILL.md +++ b/src/workflows/4-production/gds-dev-story/SKILL.md @@ -13,8 +13,8 @@ Resolve `inject` and `additional_resources` from customization: Run: `python3 scripts/resolve-customization.py gds-dev-story --key inject --key additional_resources` Use the JSON output as resolved values. -If `inject.before` is not empty, incorporate its content as high-priority context. -If `additional_resources` is not empty, read each listed file and incorporate as reference context. +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. @@ -23,4 +23,4 @@ Follow the instructions in ./workflow.md. After the workflow completes, resolve `inject.after` from customization: Run: `python3 scripts/resolve-customization.py gds-dev-story --key inject.after` -If resolved `inject.after` is not empty, incorporate its content as a final checklist or validation gate. +If resolved `inject.after` is not empty, append it to your active instructions and follow it. diff --git a/src/workflows/4-production/gds-dev-story/scripts/resolve-customization.py b/src/workflows/4-production/gds-dev-story/scripts/resolve-customization.py index c102b15..d9994a5 100755 --- a/src/workflows/4-production/gds-dev-story/scripts/resolve-customization.py +++ b/src/workflows/4-production/gds-dev-story/scripts/resolve-customization.py @@ -91,7 +91,7 @@ def deep_merge(base: dict[str, Any], override: dict[str, Any]) -> dict[str, Any] 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) + merged[key] = merge_menu(base_val, over_val) # type: ignore[arg-type] else: merged[key] = over_val diff --git a/src/workflows/4-production/gds-retrospective/SKILL.md b/src/workflows/4-production/gds-retrospective/SKILL.md index 746bacc..799811a 100644 --- a/src/workflows/4-production/gds-retrospective/SKILL.md +++ b/src/workflows/4-production/gds-retrospective/SKILL.md @@ -13,8 +13,8 @@ Resolve `inject` and `additional_resources` from customization: Run: `python3 scripts/resolve-customization.py gds-retrospective --key inject --key additional_resources` Use the JSON output as resolved values. -If `inject.before` is not empty, incorporate its content as high-priority context. -If `additional_resources` is not empty, read each listed file and incorporate as reference context. +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. @@ -23,4 +23,4 @@ Follow the instructions in ./workflow.md. After the workflow completes, resolve `inject.after` from customization: Run: `python3 scripts/resolve-customization.py gds-retrospective --key inject.after` -If resolved `inject.after` is not empty, incorporate its content as a final checklist or validation gate. +If resolved `inject.after` is not empty, append it to your active instructions and follow it. diff --git a/src/workflows/4-production/gds-retrospective/scripts/resolve-customization.py b/src/workflows/4-production/gds-retrospective/scripts/resolve-customization.py index c102b15..d9994a5 100755 --- a/src/workflows/4-production/gds-retrospective/scripts/resolve-customization.py +++ b/src/workflows/4-production/gds-retrospective/scripts/resolve-customization.py @@ -91,7 +91,7 @@ def deep_merge(base: dict[str, Any], override: dict[str, Any]) -> dict[str, Any] 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) + merged[key] = merge_menu(base_val, over_val) # type: ignore[arg-type] else: merged[key] = over_val diff --git a/src/workflows/4-production/gds-sprint-planning/SKILL.md b/src/workflows/4-production/gds-sprint-planning/SKILL.md index 68baaef..0bdcc1d 100644 --- a/src/workflows/4-production/gds-sprint-planning/SKILL.md +++ b/src/workflows/4-production/gds-sprint-planning/SKILL.md @@ -13,8 +13,8 @@ Resolve `inject` and `additional_resources` from customization: Run: `python3 scripts/resolve-customization.py gds-sprint-planning --key inject --key additional_resources` Use the JSON output as resolved values. -If `inject.before` is not empty, incorporate its content as high-priority context. -If `additional_resources` is not empty, read each listed file and incorporate as reference context. +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. @@ -23,4 +23,4 @@ Follow the instructions in ./workflow.md. After the workflow completes, resolve `inject.after` from customization: Run: `python3 scripts/resolve-customization.py gds-sprint-planning --key inject.after` -If resolved `inject.after` is not empty, incorporate its content as a final checklist or validation gate. +If resolved `inject.after` is not empty, append it to your active instructions and follow it. diff --git a/src/workflows/4-production/gds-sprint-planning/scripts/resolve-customization.py b/src/workflows/4-production/gds-sprint-planning/scripts/resolve-customization.py index c102b15..d9994a5 100755 --- a/src/workflows/4-production/gds-sprint-planning/scripts/resolve-customization.py +++ b/src/workflows/4-production/gds-sprint-planning/scripts/resolve-customization.py @@ -91,7 +91,7 @@ def deep_merge(base: dict[str, Any], override: dict[str, Any]) -> dict[str, Any] 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) + merged[key] = merge_menu(base_val, over_val) # type: ignore[arg-type] else: merged[key] = over_val diff --git a/src/workflows/4-production/gds-sprint-status/SKILL.md b/src/workflows/4-production/gds-sprint-status/SKILL.md index 8213077..5af1340 100644 --- a/src/workflows/4-production/gds-sprint-status/SKILL.md +++ b/src/workflows/4-production/gds-sprint-status/SKILL.md @@ -13,8 +13,8 @@ Resolve `inject` and `additional_resources` from customization: Run: `python3 scripts/resolve-customization.py gds-sprint-status --key inject --key additional_resources` Use the JSON output as resolved values. -If `inject.before` is not empty, incorporate its content as high-priority context. -If `additional_resources` is not empty, read each listed file and incorporate as reference context. +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. @@ -23,4 +23,4 @@ Follow the instructions in ./workflow.md. After the workflow completes, resolve `inject.after` from customization: Run: `python3 scripts/resolve-customization.py gds-sprint-status --key inject.after` -If resolved `inject.after` is not empty, incorporate its content as a final checklist or validation gate. +If resolved `inject.after` is not empty, append it to your active instructions and follow it. diff --git a/src/workflows/4-production/gds-sprint-status/scripts/resolve-customization.py b/src/workflows/4-production/gds-sprint-status/scripts/resolve-customization.py index c102b15..d9994a5 100755 --- a/src/workflows/4-production/gds-sprint-status/scripts/resolve-customization.py +++ b/src/workflows/4-production/gds-sprint-status/scripts/resolve-customization.py @@ -91,7 +91,7 @@ def deep_merge(base: dict[str, Any], override: dict[str, Any]) -> dict[str, Any] 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) + merged[key] = merge_menu(base_val, over_val) # type: ignore[arg-type] else: merged[key] = over_val diff --git a/src/workflows/gametest/gds-e2e-scaffold/SKILL.md b/src/workflows/gametest/gds-e2e-scaffold/SKILL.md index 30f8170..29c3dca 100644 --- a/src/workflows/gametest/gds-e2e-scaffold/SKILL.md +++ b/src/workflows/gametest/gds-e2e-scaffold/SKILL.md @@ -13,8 +13,8 @@ Resolve `inject` and `additional_resources` from customization: Run: `python3 scripts/resolve-customization.py gds-e2e-scaffold --key inject --key additional_resources` Use the JSON output as resolved values. -If `inject.before` is not empty, incorporate its content as high-priority context. -If `additional_resources` is not empty, read each listed file and incorporate as reference context. +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. @@ -23,4 +23,4 @@ Follow the instructions in ./workflow.md. After the workflow completes, resolve `inject.after` from customization: Run: `python3 scripts/resolve-customization.py gds-e2e-scaffold --key inject.after` -If resolved `inject.after` is not empty, incorporate its content as a final checklist or validation gate. +If resolved `inject.after` is not empty, append it to your active instructions and follow it. diff --git a/src/workflows/gametest/gds-e2e-scaffold/scripts/resolve-customization.py b/src/workflows/gametest/gds-e2e-scaffold/scripts/resolve-customization.py index c102b15..d9994a5 100755 --- a/src/workflows/gametest/gds-e2e-scaffold/scripts/resolve-customization.py +++ b/src/workflows/gametest/gds-e2e-scaffold/scripts/resolve-customization.py @@ -91,7 +91,7 @@ def deep_merge(base: dict[str, Any], override: dict[str, Any]) -> dict[str, Any] 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) + merged[key] = merge_menu(base_val, over_val) # type: ignore[arg-type] else: merged[key] = over_val diff --git a/src/workflows/gametest/gds-performance-test/SKILL.md b/src/workflows/gametest/gds-performance-test/SKILL.md index 75e9bfe..9dee9b5 100644 --- a/src/workflows/gametest/gds-performance-test/SKILL.md +++ b/src/workflows/gametest/gds-performance-test/SKILL.md @@ -13,8 +13,8 @@ Resolve `inject` and `additional_resources` from customization: Run: `python3 scripts/resolve-customization.py gds-performance-test --key inject --key additional_resources` Use the JSON output as resolved values. -If `inject.before` is not empty, incorporate its content as high-priority context. -If `additional_resources` is not empty, read each listed file and incorporate as reference context. +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. @@ -23,4 +23,4 @@ Follow the instructions in ./workflow.md. After the workflow completes, resolve `inject.after` from customization: Run: `python3 scripts/resolve-customization.py gds-performance-test --key inject.after` -If resolved `inject.after` is not empty, incorporate its content as a final checklist or validation gate. +If resolved `inject.after` is not empty, append it to your active instructions and follow it. diff --git a/src/workflows/gametest/gds-performance-test/scripts/resolve-customization.py b/src/workflows/gametest/gds-performance-test/scripts/resolve-customization.py index c102b15..d9994a5 100755 --- a/src/workflows/gametest/gds-performance-test/scripts/resolve-customization.py +++ b/src/workflows/gametest/gds-performance-test/scripts/resolve-customization.py @@ -91,7 +91,7 @@ def deep_merge(base: dict[str, Any], override: dict[str, Any]) -> dict[str, Any] 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) + merged[key] = merge_menu(base_val, over_val) # type: ignore[arg-type] else: merged[key] = over_val diff --git a/src/workflows/gametest/gds-playtest-plan/SKILL.md b/src/workflows/gametest/gds-playtest-plan/SKILL.md index 68af6d9..0c644ab 100644 --- a/src/workflows/gametest/gds-playtest-plan/SKILL.md +++ b/src/workflows/gametest/gds-playtest-plan/SKILL.md @@ -13,8 +13,8 @@ Resolve `inject` and `additional_resources` from customization: Run: `python3 scripts/resolve-customization.py gds-playtest-plan --key inject --key additional_resources` Use the JSON output as resolved values. -If `inject.before` is not empty, incorporate its content as high-priority context. -If `additional_resources` is not empty, read each listed file and incorporate as reference context. +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. @@ -23,4 +23,4 @@ Follow the instructions in ./workflow.md. After the workflow completes, resolve `inject.after` from customization: Run: `python3 scripts/resolve-customization.py gds-playtest-plan --key inject.after` -If resolved `inject.after` is not empty, incorporate its content as a final checklist or validation gate. +If resolved `inject.after` is not empty, append it to your active instructions and follow it. diff --git a/src/workflows/gametest/gds-playtest-plan/scripts/resolve-customization.py b/src/workflows/gametest/gds-playtest-plan/scripts/resolve-customization.py index c102b15..d9994a5 100755 --- a/src/workflows/gametest/gds-playtest-plan/scripts/resolve-customization.py +++ b/src/workflows/gametest/gds-playtest-plan/scripts/resolve-customization.py @@ -91,7 +91,7 @@ def deep_merge(base: dict[str, Any], override: dict[str, Any]) -> dict[str, Any] 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) + merged[key] = merge_menu(base_val, over_val) # type: ignore[arg-type] else: merged[key] = over_val diff --git a/src/workflows/gametest/gds-test-automate/SKILL.md b/src/workflows/gametest/gds-test-automate/SKILL.md index b2cdc94..69ab30d 100644 --- a/src/workflows/gametest/gds-test-automate/SKILL.md +++ b/src/workflows/gametest/gds-test-automate/SKILL.md @@ -13,8 +13,8 @@ Resolve `inject` and `additional_resources` from customization: Run: `python3 scripts/resolve-customization.py gds-test-automate --key inject --key additional_resources` Use the JSON output as resolved values. -If `inject.before` is not empty, incorporate its content as high-priority context. -If `additional_resources` is not empty, read each listed file and incorporate as reference context. +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. @@ -23,4 +23,4 @@ Follow the instructions in ./workflow.md. After the workflow completes, resolve `inject.after` from customization: Run: `python3 scripts/resolve-customization.py gds-test-automate --key inject.after` -If resolved `inject.after` is not empty, incorporate its content as a final checklist or validation gate. +If resolved `inject.after` is not empty, append it to your active instructions and follow it. diff --git a/src/workflows/gametest/gds-test-automate/scripts/resolve-customization.py b/src/workflows/gametest/gds-test-automate/scripts/resolve-customization.py index c102b15..d9994a5 100755 --- a/src/workflows/gametest/gds-test-automate/scripts/resolve-customization.py +++ b/src/workflows/gametest/gds-test-automate/scripts/resolve-customization.py @@ -91,7 +91,7 @@ def deep_merge(base: dict[str, Any], override: dict[str, Any]) -> dict[str, Any] 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) + merged[key] = merge_menu(base_val, over_val) # type: ignore[arg-type] else: merged[key] = over_val diff --git a/src/workflows/gametest/gds-test-design/SKILL.md b/src/workflows/gametest/gds-test-design/SKILL.md index fc5beec..a41db8f 100644 --- a/src/workflows/gametest/gds-test-design/SKILL.md +++ b/src/workflows/gametest/gds-test-design/SKILL.md @@ -13,8 +13,8 @@ Resolve `inject` and `additional_resources` from customization: Run: `python3 scripts/resolve-customization.py gds-test-design --key inject --key additional_resources` Use the JSON output as resolved values. -If `inject.before` is not empty, incorporate its content as high-priority context. -If `additional_resources` is not empty, read each listed file and incorporate as reference context. +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. @@ -23,4 +23,4 @@ Follow the instructions in ./workflow.md. After the workflow completes, resolve `inject.after` from customization: Run: `python3 scripts/resolve-customization.py gds-test-design --key inject.after` -If resolved `inject.after` is not empty, incorporate its content as a final checklist or validation gate. +If resolved `inject.after` is not empty, append it to your active instructions and follow it. diff --git a/src/workflows/gametest/gds-test-design/scripts/resolve-customization.py b/src/workflows/gametest/gds-test-design/scripts/resolve-customization.py index c102b15..d9994a5 100755 --- a/src/workflows/gametest/gds-test-design/scripts/resolve-customization.py +++ b/src/workflows/gametest/gds-test-design/scripts/resolve-customization.py @@ -91,7 +91,7 @@ def deep_merge(base: dict[str, Any], override: dict[str, Any]) -> dict[str, Any] 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) + merged[key] = merge_menu(base_val, over_val) # type: ignore[arg-type] else: merged[key] = over_val diff --git a/src/workflows/gametest/gds-test-framework/SKILL.md b/src/workflows/gametest/gds-test-framework/SKILL.md index 6629abc..1e03971 100644 --- a/src/workflows/gametest/gds-test-framework/SKILL.md +++ b/src/workflows/gametest/gds-test-framework/SKILL.md @@ -13,8 +13,8 @@ Resolve `inject` and `additional_resources` from customization: Run: `python3 scripts/resolve-customization.py gds-test-framework --key inject --key additional_resources` Use the JSON output as resolved values. -If `inject.before` is not empty, incorporate its content as high-priority context. -If `additional_resources` is not empty, read each listed file and incorporate as reference context. +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. @@ -23,4 +23,4 @@ Follow the instructions in ./workflow.md. After the workflow completes, resolve `inject.after` from customization: Run: `python3 scripts/resolve-customization.py gds-test-framework --key inject.after` -If resolved `inject.after` is not empty, incorporate its content as a final checklist or validation gate. +If resolved `inject.after` is not empty, append it to your active instructions and follow it. diff --git a/src/workflows/gametest/gds-test-framework/scripts/resolve-customization.py b/src/workflows/gametest/gds-test-framework/scripts/resolve-customization.py index c102b15..d9994a5 100755 --- a/src/workflows/gametest/gds-test-framework/scripts/resolve-customization.py +++ b/src/workflows/gametest/gds-test-framework/scripts/resolve-customization.py @@ -91,7 +91,7 @@ def deep_merge(base: dict[str, Any], override: dict[str, Any]) -> dict[str, Any] 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) + merged[key] = merge_menu(base_val, over_val) # type: ignore[arg-type] else: merged[key] = over_val diff --git a/src/workflows/gametest/gds-test-review/SKILL.md b/src/workflows/gametest/gds-test-review/SKILL.md index 4436ef4..ff3e536 100644 --- a/src/workflows/gametest/gds-test-review/SKILL.md +++ b/src/workflows/gametest/gds-test-review/SKILL.md @@ -13,8 +13,8 @@ Resolve `inject` and `additional_resources` from customization: Run: `python3 scripts/resolve-customization.py gds-test-review --key inject --key additional_resources` Use the JSON output as resolved values. -If `inject.before` is not empty, incorporate its content as high-priority context. -If `additional_resources` is not empty, read each listed file and incorporate as reference context. +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. @@ -23,4 +23,4 @@ Follow the instructions in ./workflow.md. After the workflow completes, resolve `inject.after` from customization: Run: `python3 scripts/resolve-customization.py gds-test-review --key inject.after` -If resolved `inject.after` is not empty, incorporate its content as a final checklist or validation gate. +If resolved `inject.after` is not empty, append it to your active instructions and follow it. diff --git a/src/workflows/gametest/gds-test-review/scripts/resolve-customization.py b/src/workflows/gametest/gds-test-review/scripts/resolve-customization.py index c102b15..d9994a5 100755 --- a/src/workflows/gametest/gds-test-review/scripts/resolve-customization.py +++ b/src/workflows/gametest/gds-test-review/scripts/resolve-customization.py @@ -91,7 +91,7 @@ def deep_merge(base: dict[str, Any], override: dict[str, Any]) -> dict[str, Any] 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) + merged[key] = merge_menu(base_val, over_val) # type: ignore[arg-type] else: merged[key] = over_val diff --git a/src/workflows/gds-document-project/SKILL.md b/src/workflows/gds-document-project/SKILL.md index 8c4c241..c725b74 100644 --- a/src/workflows/gds-document-project/SKILL.md +++ b/src/workflows/gds-document-project/SKILL.md @@ -13,8 +13,8 @@ Resolve `inject` and `additional_resources` from customization: Run: `python3 scripts/resolve-customization.py gds-document-project --key inject --key additional_resources` Use the JSON output as resolved values. -If `inject.before` is not empty, incorporate its content as high-priority context. -If `additional_resources` is not empty, read each listed file and incorporate as reference context. +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. @@ -23,4 +23,4 @@ Follow the instructions in ./workflow.md. After the workflow completes, resolve `inject.after` from customization: Run: `python3 scripts/resolve-customization.py gds-document-project --key inject.after` -If resolved `inject.after` is not empty, incorporate its content as a final checklist or validation gate. +If resolved `inject.after` is not empty, append it to your active instructions and follow it. diff --git a/src/workflows/gds-document-project/scripts/resolve-customization.py b/src/workflows/gds-document-project/scripts/resolve-customization.py index c102b15..d9994a5 100755 --- a/src/workflows/gds-document-project/scripts/resolve-customization.py +++ b/src/workflows/gds-document-project/scripts/resolve-customization.py @@ -91,7 +91,7 @@ def deep_merge(base: dict[str, Any], override: dict[str, Any]) -> dict[str, Any] 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) + merged[key] = merge_menu(base_val, over_val) # type: ignore[arg-type] else: merged[key] = over_val diff --git a/src/workflows/gds-quick-flow/gds-quick-dev-new-preview/SKILL.md b/src/workflows/gds-quick-flow/gds-quick-dev-new-preview/SKILL.md index 154cdd3..a4aae92 100644 --- a/src/workflows/gds-quick-flow/gds-quick-dev-new-preview/SKILL.md +++ b/src/workflows/gds-quick-flow/gds-quick-dev-new-preview/SKILL.md @@ -13,8 +13,8 @@ Resolve `inject` and `additional_resources` from customization: Run: `python3 scripts/resolve-customization.py gds-quick-dev-new-preview --key inject --key additional_resources` Use the JSON output as resolved values. -If `inject.before` is not empty, incorporate its content as high-priority context. -If `additional_resources` is not empty, read each listed file and incorporate as reference context. +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. @@ -23,4 +23,4 @@ Follow the instructions in ./workflow.md. After the workflow completes, resolve `inject.after` from customization: Run: `python3 scripts/resolve-customization.py gds-quick-dev-new-preview --key inject.after` -If resolved `inject.after` is not empty, incorporate its content as a final checklist or validation gate. +If resolved `inject.after` is not empty, append it to your active instructions and follow it. diff --git a/src/workflows/gds-quick-flow/gds-quick-dev-new-preview/scripts/resolve-customization.py b/src/workflows/gds-quick-flow/gds-quick-dev-new-preview/scripts/resolve-customization.py index c102b15..d9994a5 100755 --- a/src/workflows/gds-quick-flow/gds-quick-dev-new-preview/scripts/resolve-customization.py +++ b/src/workflows/gds-quick-flow/gds-quick-dev-new-preview/scripts/resolve-customization.py @@ -91,7 +91,7 @@ def deep_merge(base: dict[str, Any], override: dict[str, Any]) -> dict[str, Any] 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) + merged[key] = merge_menu(base_val, over_val) # type: ignore[arg-type] else: merged[key] = over_val diff --git a/src/workflows/gds-quick-flow/gds-quick-dev/SKILL.md b/src/workflows/gds-quick-flow/gds-quick-dev/SKILL.md index 238439a..2e0cc6c 100644 --- a/src/workflows/gds-quick-flow/gds-quick-dev/SKILL.md +++ b/src/workflows/gds-quick-flow/gds-quick-dev/SKILL.md @@ -13,8 +13,8 @@ Resolve `inject` and `additional_resources` from customization: Run: `python3 scripts/resolve-customization.py gds-quick-dev --key inject --key additional_resources` Use the JSON output as resolved values. -If `inject.before` is not empty, incorporate its content as high-priority context. -If `additional_resources` is not empty, read each listed file and incorporate as reference context. +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. @@ -23,4 +23,4 @@ Follow the instructions in ./workflow.md. After the workflow completes, resolve `inject.after` from customization: Run: `python3 scripts/resolve-customization.py gds-quick-dev --key inject.after` -If resolved `inject.after` is not empty, incorporate its content as a final checklist or validation gate. +If resolved `inject.after` is not empty, append it to your active instructions and follow it. diff --git a/src/workflows/gds-quick-flow/gds-quick-dev/scripts/resolve-customization.py b/src/workflows/gds-quick-flow/gds-quick-dev/scripts/resolve-customization.py index c102b15..d9994a5 100755 --- a/src/workflows/gds-quick-flow/gds-quick-dev/scripts/resolve-customization.py +++ b/src/workflows/gds-quick-flow/gds-quick-dev/scripts/resolve-customization.py @@ -91,7 +91,7 @@ def deep_merge(base: dict[str, Any], override: dict[str, Any]) -> dict[str, Any] 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) + merged[key] = merge_menu(base_val, over_val) # type: ignore[arg-type] else: merged[key] = over_val diff --git a/src/workflows/gds-quick-flow/gds-quick-spec/SKILL.md b/src/workflows/gds-quick-flow/gds-quick-spec/SKILL.md index 283c260..69b3339 100644 --- a/src/workflows/gds-quick-flow/gds-quick-spec/SKILL.md +++ b/src/workflows/gds-quick-flow/gds-quick-spec/SKILL.md @@ -13,8 +13,8 @@ Resolve `inject` and `additional_resources` from customization: Run: `python3 scripts/resolve-customization.py gds-quick-spec --key inject --key additional_resources` Use the JSON output as resolved values. -If `inject.before` is not empty, incorporate its content as high-priority context. -If `additional_resources` is not empty, read each listed file and incorporate as reference context. +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. @@ -23,4 +23,4 @@ Follow the instructions in ./workflow.md. After the workflow completes, resolve `inject.after` from customization: Run: `python3 scripts/resolve-customization.py gds-quick-spec --key inject.after` -If resolved `inject.after` is not empty, incorporate its content as a final checklist or validation gate. +If resolved `inject.after` is not empty, append it to your active instructions and follow it. diff --git a/src/workflows/gds-quick-flow/gds-quick-spec/scripts/resolve-customization.py b/src/workflows/gds-quick-flow/gds-quick-spec/scripts/resolve-customization.py index c102b15..d9994a5 100755 --- a/src/workflows/gds-quick-flow/gds-quick-spec/scripts/resolve-customization.py +++ b/src/workflows/gds-quick-flow/gds-quick-spec/scripts/resolve-customization.py @@ -91,7 +91,7 @@ def deep_merge(base: dict[str, Any], override: dict[str, Any]) -> dict[str, Any] 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) + merged[key] = merge_menu(base_val, over_val) # type: ignore[arg-type] else: merged[key] = over_val From cc54fb7189447130ad1681d5f6787c742cad8f9d Mon Sep 17 00:00:00 2001 From: Scott Jennings Date: Tue, 14 Apr 2026 21:12:08 -0500 Subject: [PATCH 7/7] fix(README): wrap contact email in autolink brackets Unblocks markdownlint MD034/no-bare-urls failure on CI. Same fix as the one already on main (via PR #21). Cherry-picked here so this branch's CI passes; a future rebase onto main will absorb it. Co-Authored-By: Claude Opus 4.6 (1M context) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ab9e60d..5d84ce0 100644 --- a/README.md +++ b/README.md @@ -113,7 +113,7 @@ BMGD: Great choice! Here's what I recommend... ## Support BMad -BMad is free for everyone and always will be. Star this repo, [buy me a coffee](https://buymeacoffee.com/bmad), or email contact@bmadcode.com for corporate sponsorship. +BMad is free for everyone and always will be. Star this repo, [buy me a coffee](https://buymeacoffee.com/bmad), or email for corporate sponsorship. ## License