#!/usr/bin/env python3 """Validate Codex Game Studios plugin structure and package hygiene.""" from __future__ import annotations import json import subprocess import sys from pathlib import Path ROOT = Path(__file__).resolve().parents[1] FORBIDDEN_NAMES = {".DS_Store", ".env", "plan.md"} FORBIDDEN_PARTS = {".git", "__pycache__"} EXPECTED_SKILLS = { "brainstorm", } REQUIRED_RUNTIME_AGENTS = { "art-director.toml", "creative-director.toml", "producer.toml", "technical-director.toml", } 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" 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 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)}") 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 main() -> int: assert_manifest() assert_skill_set() for skill_name in sorted(EXPECTED_SKILLS): assert_skill(skill_name) assert_runtime_agents() assert_package_hygiene() print("Codex Game Studios plugin validation passed.") return 0 if __name__ == "__main__": raise SystemExit(main())