Files
codex-game-studios/scripts/validate_plugin.py
2026-05-18 19:21:55 -07:00

176 lines
5.3 KiB
Python
Executable File

#!/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_project_template() -> None:
template_path = ROOT / "project-template" / "AGENTS.md"
if not template_path.is_file():
fail("Missing project-template/AGENTS.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",
]
for fragment in required_fragments:
if fragment not in text:
fail(f"project-template/AGENTS.md missing required content: {fragment}")
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 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_runtime_installer()
assert_package_hygiene()
print("Codex Game Studios plugin validation passed.")
return 0
if __name__ == "__main__":
raise SystemExit(main())