#!/usr/bin/env python3 """Apply a model profile to agent definition files. Supports: - OpenCode YAML frontmatter agents in .opencode/agents/*.md - Codex TOML agents in codex_adapter_templates/codex/agents/*.toml """ from __future__ import annotations import argparse import re import sys from pathlib import Path REPO_ROOT = Path(__file__).resolve().parent.parent if str(REPO_ROOT) not in sys.path: sys.path.insert(0, str(REPO_ROOT)) from scripts.lib.model_config import ( ModelConfigError, parse_model_overrides, resolve_model_profile, ) OPENCODE_ROLE_TO_FILE = { "dr_plan": ".opencode/agents/dr-plan.md", "dr_pm": ".opencode/agents/dr-pm.md", "dr_searcher": ".opencode/agents/dr-searcher.md", "dr_analyst": ".opencode/agents/dr-analyst.md", "dr_verifier": ".opencode/agents/dr-verifier.md", "dr_chief_editor": ".opencode/agents/dr-chief-editor.md", "dr_editor_in_chief": ".opencode/agents/dr-editor-in-chief.md", "dr_reporter": ".opencode/agents/dr-reporter.md", } CODEX_ROLE_TO_FILE = { "dr_plan": "codex_adapter_templates/codex/agents/dr-plan.toml", "dr_pm": "codex_adapter_templates/codex/agents/dr-pm.toml", "dr_searcher": "codex_adapter_templates/codex/agents/dr-searcher.toml", "dr_analyst": "codex_adapter_templates/codex/agents/dr-analyst.toml", "dr_verifier": "codex_adapter_templates/codex/agents/dr-verifier.toml", "dr_chief_editor": "codex_adapter_templates/codex/agents/dr-chief-editor.toml", "dr_editor_in_chief": "codex_adapter_templates/codex/agents/dr-editor-in-chief.toml", "dr_reporter": "codex_adapter_templates/codex/agents/dr-reporter.toml", } def replace_opencode_model(path: Path, model: str) -> bool: text = path.read_text(encoding="utf-8") new_text, count = re.subn(r"(?m)^model:\s*.+$", f"model: {model}", text, count=1) if count == 0: raise SystemExit(f"failed to locate model field: {path}") if new_text == text: return False path.write_text(new_text, encoding="utf-8") return True def replace_codex_model(path: Path, model: str) -> bool: text = path.read_text(encoding="utf-8") new_text, count = re.subn(r'(?m)^model\s*=\s*"[^"]+"$', f'model = "{model}"', text, count=1) if count == 0: raise SystemExit(f"failed to locate model field: {path}") if new_text == text: return False path.write_text(new_text, encoding="utf-8") return True def main() -> int: parser = argparse.ArgumentParser(description="Apply model profile to agent files") parser.add_argument("--profile", required=True, help="Profile name from configs/models.yaml") parser.add_argument("--target", choices=["opencode", "codex", "both"], default="both") parser.add_argument( "--model-override", action="append", default=[], metavar="ROLE=MODEL", help="Override one role model, repeatable", ) parser.add_argument("--dry-run", action="store_true") args = parser.parse_args() try: resolved = resolve_model_profile( profile=args.profile, overrides=parse_model_overrides(args.model_override), ) except ModelConfigError as exc: raise SystemExit(f"model profile resolution failed: {exc}") from exc roles = resolved["roles"] changed: list[str] = [] def apply_map(mapping: dict[str, str], mode: str) -> None: for role, rel_path in mapping.items(): model = roles.get(role) if not model: continue file_path = REPO_ROOT / rel_path if not file_path.exists(): continue if args.dry_run: changed.append(f"{mode}:{rel_path} -> {model}") continue did_change = replace_opencode_model(file_path, model) if mode == "opencode" else replace_codex_model(file_path, model) if did_change: changed.append(f"{mode}:{rel_path} -> {model}") if args.target in ("opencode", "both"): apply_map(OPENCODE_ROLE_TO_FILE, "opencode") if args.target in ("codex", "both"): apply_map(CODEX_ROLE_TO_FILE, "codex") print(f"Profile applied: {resolved['profile']}") print(f"Target: {args.target}") if changed: print("Updated:") for item in changed: print(f" - {item}") else: print("No files changed.") return 0 if __name__ == "__main__": raise SystemExit(main())