85 lines
4 KiB
Python
85 lines
4 KiB
Python
#!/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())
|