#!/usr/bin/env python3 """Validate Codex Game Studios plugin structure and package hygiene.""" from __future__ import annotations import json import subprocess import sys import tomllib from pathlib import Path ROOT = Path(__file__).resolve().parents[1] FORBIDDEN_NAMES = {".DS_Store", ".env", "plan.md"} FORBIDDEN_PARTS = {".git", "__pycache__"} EXPECTED_SKILLS = { "art-bible", "brainstorm", "setup-engine", } REQUIRED_RUNTIME_AGENTS = { "art-director.toml", "creative-director.toml", "godot-csharp-specialist.toml", "godot-gdextension-specialist.toml", "godot-gdscript-specialist.toml", "godot-shader-specialist.toml", "godot-specialist.toml", "producer.toml", "technical-director.toml", "technical-artist.toml", "ue-blueprint-specialist.toml", "ue-gas-specialist.toml", "ue-replication-specialist.toml", "ue-umg-specialist.toml", "unity-addressables-specialist.toml", "unity-dots-specialist.toml", "unity-shader-specialist.toml", "unity-specialist.toml", "unity-ui-specialist.toml", "unreal-specialist.toml", "ux-designer.toml", } DIRECTOR_RUNTIME_AGENTS = { "art-director.toml", "creative-director.toml", "producer.toml", "technical-director.toml", } EXPECTED_ENGINES = { "godot", "unity", "unreal", } REQUIRED_ENGINE_REFERENCE_FILES = { "VERSION.md", "breaking-changes.md", "current-best-practices.md", "deprecated-apis.md", } FORBIDDEN_UNAVAILABLE_WORKFLOW_REFS = { "$architecture-decision", "$architecture-review", "$code-review", "$design-system", "$dev-story", "$map-systems", "$propagate-design-change", "$prototype", "$sprint-plan", "$team-ui", "$test-setup", "$ux-design", "$ux-review", "/gate-check", } FORBIDDEN_UNAVAILABLE_ROLE_REFS = { "accessibility-specialist", "audio-director", "devops-engineer", "engine-programmer", "game-designer", "gameplay-programmer", "lead-programmer", "level-designer", "localization-lead", "network-programmer", "performance-analyst", "prototyper", "qa-lead", "qa-tester", "security-engineer", "sound-designer", "systems-designer", "tools-programmer", "ui-programmer", "world-builder", "writer", } def fail(message: str) -> None: print(f"ERROR: {message}", file=sys.stderr) raise SystemExit(1) def run_git(args: list[str]) -> list[str]: result = subprocess.run( ["git", *args], cwd=ROOT, text=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, ) if result.returncode != 0: raise RuntimeError(result.stderr.strip() or "git command failed") return [line for line in result.stdout.splitlines() if line] def assert_manifest() -> None: manifest_path = ROOT / ".codex-plugin" / "plugin.json" if not manifest_path.is_file(): fail("Missing .codex-plugin/plugin.json") manifest = json.loads(manifest_path.read_text(encoding="utf-8")) if manifest.get("name") != "codex-game-studios": fail("plugin.json name must be codex-game-studios") if manifest.get("skills") != "./skills/": fail('plugin.json must expose skills via "./skills/"') for prompt in manifest.get("interface", {}).get("defaultPrompt", []): if len(prompt) > 128: fail(f"defaultPrompt exceeds 128 chars: {prompt}") def assert_skill(skill_name: str) -> None: skill_dir = ROOT / "skills" / skill_name skill_path = skill_dir / "SKILL.md" details_path = skill_dir / "DETAILS.md" agent_path = skill_dir / "agents" / "openai.yaml" if not skill_path.is_file(): fail(f"Missing skill file: {skill_path.relative_to(ROOT)}") if not details_path.is_file(): fail(f"Missing skill details file: {details_path.relative_to(ROOT)}") if not agent_path.is_file(): fail(f"Missing skill UI metadata: {agent_path.relative_to(ROOT)}") text = skill_path.read_text(encoding="utf-8") if not text.startswith("---\n"): fail(f"Missing YAML frontmatter: {skill_path.relative_to(ROOT)}") if f"name: {skill_name}" not in text: fail(f"Skill name mismatch in {skill_path.relative_to(ROOT)}") if "description:" not in text.split("---", 2)[1]: fail(f"Missing skill description in {skill_path.relative_to(ROOT)}") def assert_skill_set() -> None: present = { path.parent.name for path in (ROOT / "skills").glob("*/SKILL.md") } if present != EXPECTED_SKILLS: fail( "Skill set mismatch. Expected exactly: " + ", ".join(sorted(EXPECTED_SKILLS)) + "; found: " + ", ".join(sorted(present)) ) def assert_runtime_agents() -> None: agent_dir = ROOT / "runtime" / "agents" if not agent_dir.is_dir(): fail("Missing runtime/agents directory") present = {path.name for path in agent_dir.glob("*.toml")} missing = sorted(REQUIRED_RUNTIME_AGENTS - present) if missing: fail(f"Missing runtime agents: {', '.join(missing)}") for agent_name in sorted(REQUIRED_RUNTIME_AGENTS): text = (agent_dir / agent_name).read_text(encoding="utf-8") try: parsed = tomllib.loads(text) except tomllib.TOMLDecodeError as error: fail(f"{agent_name} is not valid TOML: {error}") for field_name in ["name", "description", "developer_instructions"]: if field_name not in parsed: fail(f"{agent_name} missing TOML field: {field_name}") if "specialist" in agent_name and "Version Awareness" not in text: fail(f"{agent_name} missing Version Awareness instructions") forbidden_roles = sorted( role for role in FORBIDDEN_UNAVAILABLE_ROLE_REFS if role in text ) if agent_name not in DIRECTOR_RUNTIME_AGENTS and forbidden_roles: fail( f"{agent_name} references unavailable role agents: " + ", ".join(forbidden_roles) ) forbidden_workflows = sorted( ref for ref in FORBIDDEN_UNAVAILABLE_WORKFLOW_REFS if ref in text ) if agent_name not in DIRECTOR_RUNTIME_AGENTS and forbidden_workflows: fail( f"{agent_name} references unavailable workflows: " + ", ".join(forbidden_workflows) ) def assert_project_template() -> None: template_path = ROOT / "project-template" / "AGENTS.md" tech_template_path = ROOT / "project-template" / "technical-preferences.md" if not template_path.is_file(): fail("Missing project-template/AGENTS.md") if not tech_template_path.is_file(): fail("Missing project-template/technical-preferences.md") text = template_path.read_text(encoding="utf-8") required_fragments = [ "Codex Game Studios Project Guide", "production/review-mode.txt", ".codex/agents/", "design/gdd/game-concept.md", "design/gdd/game-pillars.md", "docs/technical-preferences.md", "docs/engine-reference//VERSION.md", "$setup-engine", ] for fragment in required_fragments: if fragment not in text: fail(f"project-template/AGENTS.md missing required content: {fragment}") legacy_import = "@." + "cla" + "ude" for fragment in [legacy_import, "coordination-rules", "coding-standards"]: if fragment in text: fail(f"project-template/AGENTS.md contains unavailable global import: {fragment}") forbidden_workflows = sorted( ref for ref in FORBIDDEN_UNAVAILABLE_WORKFLOW_REFS if ref in text ) if forbidden_workflows: fail( "project-template/AGENTS.md references unavailable workflows: " + ", ".join(forbidden_workflows) ) tech_text = tech_template_path.read_text(encoding="utf-8") required_tech_fragments = [ "# Technical Preferences", "## Engine & Language", "## Input & Platform", "## Engine Specialists", "File Extension Routing", ] for fragment in required_tech_fragments: if fragment not in tech_text: fail( "project-template/technical-preferences.md missing required " f"content: {fragment}" ) forbidden_tech_workflows = sorted( ref for ref in FORBIDDEN_UNAVAILABLE_WORKFLOW_REFS if ref in tech_text ) if forbidden_tech_workflows: fail( "project-template/technical-preferences.md references unavailable " "workflows: " + ", ".join(forbidden_tech_workflows) ) setup_details = ROOT / "skills" / "setup-engine" / "DETAILS.md" if setup_details.is_file(): setup_text = setup_details.read_text(encoding="utf-8") required_setup_fragments = [ "technical utility skill", "has no director gates", "Existing Configuration Check", "Specific section only", "May I write these changes to `docs/technical-preferences.md`?", "Rendering and Physics", "Verdict: COMPLETE", ] for fragment in required_setup_fragments: if fragment not in setup_text: fail( "skills/setup-engine/DETAILS.md missing setup-engine " f"behavior: {fragment}" ) forbidden_setup_workflows = sorted( ref for ref in FORBIDDEN_UNAVAILABLE_WORKFLOW_REFS if ref in setup_text ) if forbidden_setup_workflows: fail( "skills/setup-engine/DETAILS.md references unavailable workflows: " + ", ".join(forbidden_setup_workflows) ) def assert_art_bible_workflow() -> None: details_path = ROOT / "skills" / "art-bible" / "DETAILS.md" template_path = ROOT / "references" / "studio-docs" / "templates" / "art-bible.md" if not details_path.is_file(): fail("Missing skills/art-bible/DETAILS.md") if not template_path.is_file(): fail("Missing references/studio-docs/templates/art-bible.md") details_text = details_path.read_text(encoding="utf-8") required_details = [ "design/gdd/game-concept.md", "design/art/art-bible.md", "docs/technical-preferences.md", "Retrofit Mode", "AD-ART-BIBLE", "Codex subagent workflow", "May I write section", "technical-artist", "ux-designer", "$setup-engine", "Stop here", "Verdict: COMPLETE", ] for fragment in required_details: if fragment not in details_text: fail( "skills/art-bible/DETAILS.md missing art-bible behavior: " f"{fragment}" ) forbidden_details = [ "/map-systems", "/design-system", "/asset-spec", "/consistency-check", "/create-architecture", "Ask" + "User" + "Question", ] present_forbidden_details = sorted( fragment for fragment in forbidden_details if fragment in details_text ) if present_forbidden_details: fail( "skills/art-bible/DETAILS.md references unavailable or non-native " "workflow terms: " + ", ".join(present_forbidden_details) ) forbidden_workflows = sorted( ref for ref in FORBIDDEN_UNAVAILABLE_WORKFLOW_REFS if ref in details_text ) if forbidden_workflows: fail( "skills/art-bible/DETAILS.md references unavailable workflows: " + ", ".join(forbidden_workflows) ) template_text = template_path.read_text(encoding="utf-8") required_template = [ "Visual Identity Statement", "Mood & Atmosphere", "Shape Language", "Color System", "Character Design Direction", "Environment Design Language", "UI/HUD Visual Direction", "Asset Standards", "Reference Direction", "AD-ART-BIBLE", "$setup-engine", "Stop here", ] for fragment in required_template: if fragment not in template_text: fail( "references/studio-docs/templates/art-bible.md missing required " f"content: {fragment}" ) def assert_engine_references() -> None: reference_root = ROOT / "references" / "engine-reference" if not reference_root.is_dir(): fail("Missing references/engine-reference directory") present = {path.name for path in reference_root.iterdir() if path.is_dir()} missing_engines = sorted(EXPECTED_ENGINES - present) if missing_engines: fail(f"Missing engine reference baselines: {', '.join(missing_engines)}") for engine in sorted(EXPECTED_ENGINES): engine_dir = reference_root / engine missing_files = sorted( file_name for file_name in REQUIRED_ENGINE_REFERENCE_FILES if not (engine_dir / file_name).is_file() ) if missing_files: fail( f"Missing {engine} reference files: " + ", ".join(missing_files) ) if not (engine_dir / "modules").is_dir(): fail(f"Missing {engine} reference modules directory") technical_template = ( ROOT / "references" / "studio-docs" / "templates" / "technical-preferences.md" ) if not technical_template.is_file(): fail("Missing references/studio-docs/templates/technical-preferences.md") technical_template_text = technical_template.read_text(encoding="utf-8") forbidden_template_workflows = sorted( ref for ref in FORBIDDEN_UNAVAILABLE_WORKFLOW_REFS if ref in technical_template_text ) if forbidden_template_workflows: fail( "references/studio-docs/templates/technical-preferences.md " "references unavailable workflows: " + ", ".join(forbidden_template_workflows) ) def assert_runtime_installer() -> None: installer_path = ROOT / "scripts" / "install_codex_runtime.py" if not installer_path.is_file(): fail("Missing scripts/install_codex_runtime.py") text = installer_path.read_text(encoding="utf-8") required_fragments = [ "PROJECT_AGENTS_TEMPLATE", "project-template", "AGENTS.md", "--force-agents-md", ] for fragment in required_fragments: if fragment not in text: fail(f"install_codex_runtime.py missing required content: {fragment}") def assert_package_hygiene() -> None: try: tracked = run_git(["ls-files"]) untracked = run_git(["ls-files", "--others", "--exclude-standard"]) candidates = tracked + untracked except RuntimeError: candidates = [ str(path.relative_to(ROOT)) for path in ROOT.rglob("*") if path.is_file() or path.is_dir() ] bad: list[str] = [] for rel_path in candidates: path = Path(rel_path) if path.name in FORBIDDEN_NAMES or FORBIDDEN_PARTS.intersection(path.parts): bad.append(rel_path) if bad: fail("Forbidden package candidates:\n- " + "\n- ".join(sorted(bad))) def assert_native_codex_language() -> None: try: tracked = run_git(["ls-files"]) untracked = run_git(["ls-files", "--others", "--exclude-standard"]) candidates = tracked + untracked except RuntimeError: candidates = [ str(path.relative_to(ROOT)) for path in ROOT.rglob("*") if path.is_file() ] blocked_terms = [ "C" + "laude", "C" + "LAUDE", "." + "cla" + "ude", ] bad: list[str] = [] for rel_path in candidates: path = ROOT / rel_path if not path.is_file(): continue try: text = path.read_text(encoding="utf-8") except UnicodeDecodeError: continue for term in blocked_terms: if term in text: bad.append(rel_path) break if bad: fail( "Files contain non-native Codex terminology:\n- " + "\n- ".join(sorted(bad)) ) def main() -> int: assert_manifest() assert_skill_set() for skill_name in sorted(EXPECTED_SKILLS): assert_skill(skill_name) assert_runtime_agents() assert_project_template() assert_art_bible_workflow() assert_engine_references() assert_runtime_installer() assert_package_hygiene() assert_native_codex_language() print("Codex Game Studios plugin validation passed.") return 0 if __name__ == "__main__": raise SystemExit(main())