#!/usr/bin/env python3 """Check role/model and context-policy drift; optionally validate the live OpenCode loader.""" from __future__ import annotations import argparse import json import os from pathlib import Path import subprocess import sys ROOT = Path(__file__).resolve().parent.parent def check(root: Path, resolved: bool = False) -> list[str]: config = json.loads((root / "opencode.json").read_text()) registry = json.loads((root / "campaign/models.json").read_text()) errors = [] if config.get("$schema") != "https://opencode.ai/config.json": errors.append("OpenCode schema reference is missing") if config.get("model") != "openai/gpt-5.6-sol": errors.append("project loop model must be GPT-5.6 Sol") for role, model in registry["roles"].items(): name = registry["agents"][role] agent = config.get("agent", {}).get(name, {}) if agent.get("model") != model: errors.append(f"{role}: registry/config model mismatch") if agent.get("steps") != registry["max_steps"]: errors.append(f"{role}: quantum size mismatch") prompt = agent.get("prompt", "") if not prompt.startswith("{file:./campaign/agents/") or not prompt.endswith("}"): errors.append(f"{role}: prompt must come from versioned campaign/agents") elif not (root / prompt[6:-1]).is_file(): errors.append(f"{role}: missing prompt file") if role not in ("lead", "architecture-review") and agent.get("permission", {}).get("task") != "deny": errors.append(f"{role}: nested delegation is not disabled") for role in ("lead", "architecture-review", "analyst", "implementer", "verifier", "lab"): if registry["roles"].get(role) != "openai/gpt-5.6-sol": errors.append(f"{role}: normal loop must use GPT-5.6 Sol") if registry["roles"].get("resolver") != "openai/gpt-6-astra": errors.append("resolver: problem resolution must use GPT-6 Astra") compaction = config.get("compaction", {}) if not compaction.get("auto") or compaction.get("reserved", 0) < 30000: errors.append("automatic compaction requires at least 30000 reserved tokens") if compaction.get("tail_turns", 99) > 4: errors.append("compaction retains more than four recent turns") if resolved: env = os.environ.copy() env["OPENCODE_CONFIG"] = str(root / "opencode.json") # Caller overrides must not make a checked configuration look correct by accident. env.pop("OPENCODE_CONFIG_CONTENT", None) data = json.loads(subprocess.check_output( ["opencode", "debug", "config"], cwd=root, env=env, text=True)) for role, model in registry["roles"].items(): actual = data.get("agent", {}).get(registry["agents"][role], {}) if actual.get("model") != model: errors.append(f"{role}: resolved loader model mismatch") if "{file:" in actual.get("prompt", "") or not actual.get("prompt"): errors.append(f"{role}: loader did not resolve prompt") available = set(subprocess.check_output(["opencode", "models"], cwd=root, env=env, text=True).splitlines()) for model in set(registry["roles"].values()): if model not in available: errors.append(f"unavailable model: {model}") return errors def main(): parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--resolved", action="store_true") args = parser.parse_args() try: errors = check(ROOT, args.resolved) except (OSError, ValueError, KeyError, subprocess.SubprocessError) as exc: print(f"agent configuration failed: {exc}", file=sys.stderr) return 1 for error in errors: print(error, file=sys.stderr) if not errors: print("Agent routing, prompts and context policy: PASS" + (" (live loader and models checked)" if args.resolved else "")) return bool(errors) if __name__ == "__main__": sys.exit(main())