#!/usr/bin/env python3 """Platform-neutral Deep Research CLI for Codex and other adapters. This CLI intentionally keeps deterministic orchestration in Python while allowing Codex/OpenCode/Gemini/Claude Code to provide the agentic layer. """ from __future__ import annotations import argparse import json import re import subprocess import sys from datetime import datetime, timezone 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, list_model_profiles, parse_model_overrides, resolve_model_profile, ) from scripts.runtime.assembly import build_chapter_briefs, build_compressed_findings, run_chapter_assembly_workers from scripts.runtime.orchestrator import create_phase2_task_cards, write_placeholder_packets from scripts.runtime.methods import ResearchMethodRegistry from scripts.runtime.phase1 import create_project, render_framework, write_material_brief from scripts.runtime.review import build_phase3_critique from scripts.runtime.roles import resolve_runtime_profile from scripts.runtime.sources import rebuild_sources_from_packets from scripts.runtime.skills import SkillRegistry, default_adapter_skill_dirs from scripts.runtime.tasks import TaskCard from scripts.runtime.workers import run_packet_workers PROJECTS_DIR = REPO_ROOT / "projects" CODEX_COMMAND_TEMPLATES_DIR = REPO_ROOT / "codex_adapter_templates" / "codex" / "commands" LEGACY_CODEX_COMMANDS_DIR = REPO_ROOT / ".codex" / "commands" def resolve_project(project: str | None, *, projects_dir: Path = PROJECTS_DIR) -> Path: if project: p = Path(project) if p.is_dir(): return p.resolve() cand = projects_dir / project if cand.is_dir(): return cand.resolve() raise SystemExit(f"project not found: {project}") manifests = sorted( projects_dir.glob("*/manifest.json"), key=lambda p: p.stat().st_mtime, reverse=True, ) if not manifests: raise SystemExit("no projects found") return manifests[0].parent.resolve() def load_manifest(project_root: Path) -> dict: path = project_root / "manifest.json" if not path.exists(): raise SystemExit(f"manifest not found: {path}") return json.loads(path.read_text(encoding="utf-8")) def count_words(text: str) -> int: return len(re.findall(r"[A-Za-z]+(?:[-'][A-Za-z]+)*", text)) def count_chinese_chars(text: str) -> int: return sum(1 for c in text if "\u4e00" <= c <= "\u9fff") def file_state(path: Path) -> str: return "yes" if path.exists() else "no" def packet_state_counts(project_root: Path) -> dict[str, int]: packets = sorted((project_root / "phase2" / "packets").glob("*.json")) errors = sorted((project_root / "phase2" / "packet_errors").glob("*.json")) counts = { "ready": 0, "placeholder": 0, "invalid": 0, "errors": 0, "stale_errors": 0, "total": len(packets), } ready_stems: set[str] = set() for path in packets: try: packet = json.loads(path.read_text(encoding="utf-8")) except Exception: counts["invalid"] += 1 continue has_evidence = bool( packet.get("claims") or packet.get("evidence_items") or packet.get("counter_evidence") or packet.get("source_ids") ) if has_evidence: counts["ready"] += 1 ready_stems.add(path.stem) else: counts["placeholder"] += 1 for path in errors: if path.stem in ready_stems: counts["stale_errors"] += 1 else: counts["errors"] += 1 return counts def run_cmd(cmd: list[str], *, dry_run: bool) -> int: printable = " ".join(cmd) print(f"$ {printable}") if dry_run: return 0 return subprocess.run(cmd, cwd=REPO_ROOT, check=False).returncode def cmd_init(args: argparse.Namespace) -> int: projects_dir = Path(args.projects_dir) if args.projects_dir else PROJECTS_DIR project_root = create_project( topic=args.topic, slug=args.slug, projects_dir=projects_dir, method_key=args.method, report_type=args.report_type, model_profile=args.profile, target_words=args.target_words, input_materials=args.input_material, ) print(f"Project: {project_root.name}") print(f"Created: {project_root}") print("Runtime: python-core-v0.20") print("Next: run `dr.py frame ` to generate phase1/framework.md") return 0 def cmd_frame(args: argparse.Namespace) -> int: project_root = resolve_project(args.project) if args.dry_run: manifest = load_manifest(project_root) method = ResearchMethodRegistry().get(args.method or manifest.get("research_method")) print(f"Project: {project_root.name}") print(f"Would write: phase1/framework.md") print(f"Research method: {method.key}") print(f"Chapters: {args.chapters}") return 0 path = render_framework(project_root, method_key=args.method, chapter_count=args.chapters) print(f"Project: {project_root.name}") print(f"Wrote: {path.relative_to(project_root)}") print("Pause: review and approve the framework before Phase 2.") return 0 def cmd_approve(args: argparse.Namespace) -> int: project_root = resolve_project(args.project) manifest = load_manifest(project_root) phase1 = manifest.setdefault("phase1", {}) phase1["approved"] = True phase1["requires_user_interview"] = False phase1["approved_at"] = datetime.now(timezone.utc).replace(microsecond=0).isoformat() manifest["updated_at"] = phase1["approved_at"] (project_root / "manifest.json").write_text( json.dumps(manifest, ensure_ascii=False, indent=2) + "\n", encoding="utf-8", ) print(f"Project: {project_root.name}") print("Phase 1 approved. Phase 2 research is now enabled.") return 0 def cmd_skills(args: argparse.Namespace) -> int: registry = SkillRegistry() if args.skills_cmd == "list": for name in registry.list_names(): print(name) return 0 if args.skills_cmd == "validate": result = registry.validate() print(json.dumps(result, ensure_ascii=False, indent=2)) return 0 if result["ok"] else 1 if args.skills_cmd == "sync": targets = [Path(item) for item in args.target] if args.target else default_adapter_skill_dirs() copied = registry.sync_to(targets, force=True) print(f"Synced skills: {copied}") for target in targets: print(f" {target}") return 0 raise SystemExit(f"unknown skills command: {args.skills_cmd}") def cmd_methods(args: argparse.Namespace) -> int: registry = ResearchMethodRegistry() if args.methods_cmd == "list": for name in registry.list_names(): method = registry.get(name) print(f"{method.key}: {method.name}") return 0 if args.methods_cmd == "show": method = registry.get(args.method) print(json.dumps(method.__dict__, ensure_ascii=False, indent=2)) return 0 raise SystemExit(f"unknown methods command: {args.methods_cmd}") def cmd_research(args: argparse.Namespace) -> int: project_root = resolve_project(args.project) manifest = load_manifest(project_root) if not args.force and not (manifest.get("phase1") or {}).get("approved"): raise SystemExit( "Phase 1 is not approved. Review phase1/material_brief.md and phase1/framework.md, " "then run `uv run python scripts/dr.py approve ` or pass --force." ) runtime = resolve_runtime_profile(profile=args.profile) card_dicts = create_phase2_task_cards( project_root, axes=args.axis, dry_run=args.dry_run, ) if args.execute_packets and args.dry_run: raise SystemExit("--execute-packets cannot be combined with --dry-run") if args.assemble_chapters and args.dry_run: raise SystemExit("--assemble-chapters cannot be combined with --dry-run") if args.execute_packets: from scripts.lib.zenmux_client import ZenMuxClient, load_secrets from scripts.runtime.workers import ProjectSearchProvider load_secrets() def client_factory(_role): return ZenMuxClient(log_file=project_root / "phase2" / "logs" / "packets.jsonl") def search_provider_factory(): return ProjectSearchProvider(strict_specialized=not args.allow_search_fallback) packet_count = run_packet_workers( project_root=project_root, cards=[TaskCard(**item) for item in card_dicts], runtime=runtime, client_factory=client_factory, search_provider_factory=search_provider_factory, workers=args.workers, ) elif not (args.build_briefs or args.assemble_chapters): packet_count = write_placeholder_packets(project_root, card_dicts, dry_run=args.dry_run) else: packet_count = len(list((project_root / "phase2" / "packets").glob("*.json"))) brief_count = 0 chapter_count = 0 compressed_count = 0 source_count = None if args.build_briefs or args.assemble_chapters: source_count = rebuild_sources_from_packets(project_root) briefs = build_chapter_briefs(project_root) brief_count = len(briefs) compressed_findings = build_compressed_findings(project_root) compressed_count = len(compressed_findings) if args.assemble_chapters: from scripts.lib.zenmux_client import ZenMuxClient, load_secrets load_secrets() def chapter_client_factory(_role): return ZenMuxClient(log_file=project_root / "phase2" / "logs" / "chapters.jsonl") chapter_count = run_chapter_assembly_workers( project_root=project_root, briefs=compressed_findings, runtime=runtime, client_factory=chapter_client_factory, workers=args.workers, ) print(f"Project: {project_root.name}") print(f"Runtime: python-core-v0.20") print(f"Model profile: {runtime.profile}") print(f"Workers: {args.workers}") print(f"Task cards: {len(card_dicts)}") print(f"Packets: {packet_count}") if source_count is not None: print(f"Sources rebuilt: {source_count}") if args.build_briefs or args.assemble_chapters: print(f"Chapter briefs: {brief_count}") print(f"Compressed findings: {compressed_count}") if args.assemble_chapters: print(f"Chapter drafts: {chapter_count}") if args.dry_run: print("Dry run: no files written") elif args.assemble_chapters: print("Wrote: phase2/drafts/chXX.md") elif args.execute_packets: print("Wrote: phase2/task_cards.json and validated phase2/packets/*.json") print("Next: rerun with --build-briefs to aggregate packets into chapter briefs.") elif args.build_briefs: print("Wrote: phase2/chapter_briefs/*.json and phase2/compressed_findings/*.json") print("Next: rerun with --assemble-chapters to write Chinese chapter drafts.") else: print("Wrote: phase2/task_cards.json and phase2/packets/*.json") print("Next: rerun with --execute-packets to fill packets via model workers.") return 0 def cmd_run(args: argparse.Namespace) -> int: target = args.project_or_topic projects_dir = Path(args.projects_dir) if args.projects_dir else PROJECTS_DIR try: project_root = resolve_project(target, projects_dir=projects_dir) except SystemExit: if args.dry_run: print(f"New topic detected: {target}") print("Dry run: would create project and write phase1/framework.md") return 0 project_root = create_project( topic=target, slug=args.slug, projects_dir=projects_dir, method_key=args.method, report_type=args.report_type, model_profile=args.profile or "medium", target_words=args.target_words, input_materials=args.input_material, ) framework = render_framework(project_root, chapter_count=args.chapters) print(f"Project: {project_root.name}") print("Runtime: python-core-v0.20") print(f"Created: {project_root}") print(f"Wrote: {framework.relative_to(project_root)}") print("Pause: review and approve the framework before Phase 2.") return 0 print(f"Project: {project_root.name}") print("Runtime: python-core-v0.20") print("Next command: research") if args.dry_run: print("Dry run: would inspect manifest and continue from the next incomplete phase") return 0 return cmd_research( argparse.Namespace( project=str(project_root), workers=args.workers, axis=None, profile=args.profile, execute_packets=False, allow_search_fallback=False, build_briefs=False, assemble_chapters=False, force=False, dry_run=False, ) ) def cmd_review(args: argparse.Namespace) -> int: project_root = resolve_project(args.project) path = build_phase3_critique(project_root) print(f"Project: {project_root.name}") print(f"Wrote: {path.relative_to(project_root)}") print("Pause: review critique before Phase 4.") return 0 def cmd_status(args: argparse.Namespace) -> int: project_root = resolve_project(args.project) manifest = load_manifest(project_root) slug = project_root.name drafts = sorted((project_root / "phase2" / "drafts").glob("ch*.md")) evidence = sorted((project_root / "phase2" / "evidence").glob("ch*-evidence.md")) task_cards = project_root / "phase2" / "task_cards.json" research_brief = project_root / "phase1" / "research_brief.json" compressed_findings = sorted((project_root / "phase2" / "compressed_findings").glob("ch*.json")) packet_counts = packet_state_counts(project_root) sources = project_root / "phase2" / "sources.jsonl" final_en = project_root / "phase4" / "final_en.md" final_zh = project_root / "phase4" / "final_zh.md" final_zh_polished = project_root / "phase4" / "final_zh_polished.md" glossary = project_root / "phase4" / "glossary.json" en_words = count_words(final_en.read_text(encoding="utf-8")) if final_en.exists() else 0 zh_source = final_zh_polished if final_zh_polished.exists() else final_zh zh_chars = count_chinese_chars(zh_source.read_text(encoding="utf-8")) if zh_source.exists() else 0 source_count = 0 if sources.exists(): source_count = sum(1 for line in sources.read_text(encoding="utf-8").splitlines() if line.strip()) print(f"Project: {manifest.get('topic', slug)}") print(f"Slug: {slug}") print(f"Title: {manifest.get('report_title', '(unset)')}") print(f"Type: {manifest.get('type', '(unset)')}") print() print("Phases:") for phase in ("phase1", "phase2", "phase3", "phase4"): p = manifest.get(phase, {}) print(f" {phase}: {p.get('status', 'pending')} approved={p.get('approved', False)}") print() print("Artifacts:") print(f" framework: {file_state(project_root / 'phase1' / 'framework.md')}") print(f" research_brief.json: {file_state(research_brief)}") print(f" task_cards.json: {file_state(task_cards)}") print( " packets: " f"ready={packet_counts['ready']} " f"placeholder={packet_counts['placeholder']} " f"invalid={packet_counts['invalid']} " f"errors={packet_counts['errors']} " f"stale_errors={packet_counts['stale_errors']} " f"total={packet_counts['total']}" ) print(f" drafts: {len(drafts)}") print(f" compressed findings: {len(compressed_findings)}") print(f" evidence files: {len(evidence)}") print(f" sources: {source_count}") print(f" final_en.md: {file_state(final_en)} ({en_words:,} words)") print(f" final_zh.md: {file_state(final_zh)}") print(f" final_zh_polished.md: {file_state(final_zh_polished)} ({zh_chars:,} Chinese chars)") print(f" glossary.json: {file_state(glossary)}") return 0 def cmd_prompt(args: argparse.Namespace) -> int: name = args.command if not name.startswith("dr-"): name = f"dr-{name}" candidates = [ CODEX_COMMAND_TEMPLATES_DIR / f"{name}.md", LEGACY_CODEX_COMMANDS_DIR / f"{name}.md", ] path = next((candidate for candidate in candidates if candidate.exists()), None) if path is None: raise SystemExit(f"Codex command template not found: {candidates[0]}") text = path.read_text(encoding="utf-8") if args.argument: text = text.replace("$ARGUMENTS", args.argument) else: text = text.replace("$ARGUMENTS", "") print(text) return 0 def cmd_glossary(args: argparse.Namespace) -> int: project_root = resolve_project(args.project) cmd = [ sys.executable, str(REPO_ROOT / "scripts" / "build_glossary.py"), str(project_root), "--workers", str(args.workers), ] if args.force: cmd.append("--force") if args.only: cmd += ["--only", args.only] if args.input: cmd += ["--input", args.input] if args.output: cmd += ["--output", args.output] return run_cmd(cmd, dry_run=args.dry_run) def cmd_finalize(args: argparse.Namespace) -> int: project_root = resolve_project(args.project) manifest = load_manifest(project_root) effective_profile = args.model_profile or manifest.get("model_profile") try: resolved = resolve_model_profile( profile=effective_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"] if not args.legacy_translate: cmd: list[str] = [ sys.executable, str(REPO_ROOT / "scripts" / "build_report.py"), str(project_root), "--input", args.input, ] if args.report_engine: cmd += ["--engine", args.report_engine] if args.no_docx: cmd.append("--no-docx") if args.no_pdf: cmd.append("--no-pdf") if args.dry_run: print("Chinese-native finalize plan:") print("$ " + " ".join(cmd)) if args.polish: print( "$ " + " ".join( [ sys.executable, str(REPO_ROOT / "scripts" / "polish.py"), str(project_root), "--input", args.input, "--workers", str(args.polish_workers), "--model", roles.get("polish", "anthropic/claude-sonnet-4.6"), ] ) ) return 0 if args.polish: rc = run_cmd( [ sys.executable, str(REPO_ROOT / "scripts" / "polish.py"), str(project_root), "--input", args.input, "--workers", str(args.polish_workers), "--model", roles.get("polish", "anthropic/claude-sonnet-4.6"), ], dry_run=False, ) if rc != 0: return rc return run_cmd(cmd, dry_run=False) cmd = [ sys.executable, str(REPO_ROOT / "scripts" / "phase4_pipeline.py"), str(project_root), "--translate-workers", str(args.translate_workers), "--glossary-workers", str(args.glossary_workers), "--polish-workers", str(args.polish_workers), "--translate-model", roles.get("translate", "anthropic/claude-sonnet-4.6"), "--glossary-model", roles.get("glossary", "anthropic/claude-haiku-4.5"), "--polish-model", roles.get("polish", "anthropic/claude-sonnet-4.6"), "--glossary-mode", args.glossary_mode, ] if args.dry_run: cmd.append("--dry-run") return run_cmd(cmd, dry_run=False) def cmd_models(args: argparse.Namespace) -> int: if args.list: for name in list_model_profiles(): print(name) return 0 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 if args.probe: from scripts.lib.zenmux_client import ZenMuxClient, load_secrets, normalize_zenmux_model load_secrets() results = [] with ZenMuxClient() as client: for requested_model in sorted(set(resolved["roles"].values())): api_model = normalize_zenmux_model(requested_model) try: content = client.chat_complete( model=requested_model, system="Health check.", user="Reply with OK only.", temperature=0, max_tokens=16, tag=f"models:probe:{requested_model}", ) results.append({ "requested_model": requested_model, "api_model": api_model, "ok": True, "response": content.strip()[:80], }) except Exception as exc: # noqa: BLE001 - probe should report every model. results.append({ "requested_model": requested_model, "api_model": api_model, "ok": False, "error": str(exc)[:500], }) if args.json: print(json.dumps({**resolved, "probe": results}, ensure_ascii=False, indent=2)) else: print(f"Profile: {resolved['profile']}") print("Model probe:") for item in results: status = "ok" if item["ok"] else "fail" print(f" {status} {item['requested_model']} -> {item['api_model']}") if not item["ok"]: print(f" {item['error']}") return 0 if all(item["ok"] for item in results) else 1 if args.json: print(json.dumps(resolved, ensure_ascii=False, indent=2)) return 0 print(f"Profile: {resolved['profile']}") if resolved["description"]: print(f"Description: {resolved['description']}") print("Roles:") for role in sorted(resolved["roles"]): print(f" {role}: {resolved['roles'][role]}") if resolved.get("task_types"): print("Task types:") for task_type in sorted(resolved["task_types"]): print(f" {task_type}: {resolved['task_types'][task_type]}") return 0 def cmd_apply_models(args: argparse.Namespace) -> int: cmd = [ sys.executable, str(REPO_ROOT / "scripts" / "apply_model_profile.py"), "--profile", args.profile, "--target", args.target, ] for item in args.model_override: cmd += ["--model-override", item] if args.dry_run: cmd.append("--dry-run") return run_cmd(cmd, dry_run=False) def build_parser() -> argparse.ArgumentParser: parser = argparse.ArgumentParser(description="Deep Research platform-neutral CLI") sub = parser.add_subparsers(dest="cmd", required=True) init = sub.add_parser("init", help="Initialize a Python-core research project") init.add_argument("topic", help="Research topic") init.add_argument("--slug", help="Project slug") init.add_argument("--method", help="Research method key") init.add_argument("--type", dest="report_type", default="research", help="Report type") init.add_argument("--profile", default="medium", help="Model profile name from configs/models.yaml") init.add_argument("--target-words", type=int, default=30000) init.add_argument("--input-material", action="append", default=[], help="Path or note for user-provided material") init.add_argument("--projects-dir", help="Override projects directory") init.set_defaults(func=cmd_init) approve = sub.add_parser("approve", help="Approve Phase 1 gates before Phase 2") approve.add_argument("project", help="Project slug or path") approve.set_defaults(func=cmd_approve) frame = sub.add_parser("frame", help="Generate Phase 1 framework.md") frame.add_argument("project", help="Project slug or path") frame.add_argument("--method", help="Override research method key") frame.add_argument("--chapters", type=int, default=10) frame.add_argument("--dry-run", action="store_true") frame.set_defaults(func=cmd_frame) run = sub.add_parser("run", help="Run the platform-neutral Python-core workflow") run.add_argument("project_or_topic", help="Project slug/path or new topic") run.add_argument("--workers", type=int, default=6) run.add_argument("--profile", help="Model profile name from configs/models.yaml") run.add_argument("--slug", help="Project slug when project_or_topic is new") run.add_argument("--method", help="Research method key when project_or_topic is new") run.add_argument("--type", dest="report_type", default="research", help="Report type for new project") run.add_argument("--target-words", type=int, default=30000) run.add_argument("--chapters", type=int, default=10) run.add_argument("--input-material", action="append", default=[]) run.add_argument("--projects-dir", help="Override projects directory") run.add_argument("--dry-run", action="store_true") run.set_defaults(func=cmd_run) research = sub.add_parser("research", help="Run v0.20 task-card Phase 2") research.add_argument("project", help="Project slug or path") research.add_argument("--workers", type=int, default=6) research.add_argument("--axis", action="append", help="Restrict generated task axes; repeatable") research.add_argument("--profile", help="Model profile name from configs/models.yaml") research.add_argument("--execute-packets", action="store_true", help="Call model workers to fill evidence packets") research.add_argument("--allow-search-fallback", action="store_true", help="Allow generic search fallback for specialized routes") research.add_argument("--build-briefs", action="store_true", help="Aggregate packets into chapter briefs") research.add_argument("--assemble-chapters", action="store_true", help="Call model workers to write Chinese chapter drafts") research.add_argument("--force", action="store_true", help="bypass Phase 1 approval gate") research.add_argument("--dry-run", action="store_true") research.set_defaults(func=cmd_research) skills = sub.add_parser("skills", help="Manage canonical skills") skill_sub = skills.add_subparsers(dest="skills_cmd", required=True) skill_sub.add_parser("list", help="List canonical skills").set_defaults(func=cmd_skills) skill_sub.add_parser("validate", help="Validate canonical skills").set_defaults(func=cmd_skills) skill_sync = skill_sub.add_parser("sync", help="Sync skills into adapter directories") skill_sync.add_argument("--target", action="append", help="Target skill directory; repeatable") skill_sync.set_defaults(func=cmd_skills) methods = sub.add_parser("methods", help="List and inspect research framework methods") method_sub = methods.add_subparsers(dest="methods_cmd", required=True) method_sub.add_parser("list", help="List research methods").set_defaults(func=cmd_methods) method_show = method_sub.add_parser("show", help="Show one research method") method_show.add_argument("method") method_show.set_defaults(func=cmd_methods) status = sub.add_parser("status", help="Show project status") status.add_argument("project", nargs="?", help="Project slug or path") status.set_defaults(func=cmd_status) review = sub.add_parser("review", help="Run deterministic Phase 3 review") review.add_argument("project", help="Project slug or path") review.set_defaults(func=cmd_review) prompt = sub.add_parser("prompt", help="Print a Codex command prompt template") prompt.add_argument("command", help="Command name, e.g. dr-frame or frame") prompt.add_argument("argument", nargs="?", help="Replacement for $ARGUMENTS") prompt.set_defaults(func=cmd_prompt) glossary = sub.add_parser("glossary", help="Run glossary verification") glossary.add_argument("project", help="Project slug or path") glossary.add_argument("--workers", type=int, default=4) glossary.add_argument("--force", action="store_true") glossary.add_argument("--only") glossary.add_argument("--input") glossary.add_argument("--output") glossary.add_argument("--dry-run", action="store_true") glossary.set_defaults(func=cmd_glossary) finalize = sub.add_parser("finalize", help="Run Phase 4 deterministic pipeline") finalize.add_argument("project", help="Project slug or path") finalize.add_argument("--input", default="phase4/final_zh.md", help="Chinese Markdown source for default v0.20 finalization") finalize.add_argument("--legacy-translate", action="store_true", help="Use legacy final_en -> translate -> polish pipeline") finalize.add_argument("--polish", action="store_true", help="Run optional Chinese polish step before rendering") finalize.add_argument("--report-engine", choices=["reportlab", "quarto"], default=None) finalize.add_argument("--no-docx", action="store_true") finalize.add_argument("--no-pdf", action="store_true") finalize.add_argument("--translate-workers", type=int, default=0) finalize.add_argument("--glossary-workers", type=int, default=4) finalize.add_argument("--polish-workers", type=int, default=0) finalize.add_argument( "--glossary-mode", choices=["off", "low-confidence", "full"], default="low-confidence", ) finalize.add_argument("--model-profile", help="Model profile name from configs/models.yaml") finalize.add_argument( "--model-override", action="append", default=[], metavar="ROLE=MODEL", help="Override one role model, repeatable", ) finalize.add_argument("--dry-run", action="store_true") finalize.set_defaults(func=cmd_finalize) models = sub.add_parser("models", help="Resolve and print model profile") models.add_argument("--profile", help="Profile name from configs/models.yaml") models.add_argument("--list", action="store_true", help="List available profiles") models.add_argument( "--model-override", action="append", default=[], metavar="ROLE=MODEL", help="Override one role model, repeatable", ) models.add_argument("--probe", action="store_true", help="Send tiny health checks to resolved role models") models.add_argument("--json", action="store_true", help="Emit JSON") models.set_defaults(func=cmd_models) apply_models = sub.add_parser("apply-models", help="Apply profile to agent files") apply_models.add_argument("--profile", required=True, help="Profile name from configs/models.yaml") apply_models.add_argument("--target", choices=["opencode", "codex", "both"], default="both") apply_models.add_argument( "--model-override", action="append", default=[], metavar="ROLE=MODEL", help="Override one role model, repeatable", ) apply_models.add_argument("--dry-run", action="store_true") apply_models.set_defaults(func=cmd_apply_models) return parser def main() -> int: parser = build_parser() args = parser.parse_args() return args.func(args) if __name__ == "__main__": raise SystemExit(main())