from __future__ import annotations """Codex review shim called by agents.py. argv: task_path latest_discuss_path codex_session_id work_dir """ import json import logging import os import subprocess import sys import tempfile import time from pathlib import Path from typing import Any def parse_jsonl(stdout: str) -> tuple[str, str]: session_id = "" final_message = "" text_parts: list[str] = [] for line in stdout.splitlines(): line = line.strip() if not line: continue try: event: dict[str, Any] = json.loads(line) except json.JSONDecodeError: text_parts.append(line) continue for key in ( "session_id", "sessionId", "thread_id", "threadId", "conversation_id", "conversationId", ): value = event.get(key) if isinstance(value, str) and value: session_id = value event_type = str(event.get("type", "")) if event_type in {"session.started", "session.created", "thread.started"}: value = event.get("id") or event.get("session_id") or event.get("thread_id") if isinstance(value, str) and value: session_id = value if event_type == "item.completed": item = event.get("item") if isinstance(item, dict) and item.get("type") == "agent_message": value = item.get("text") if isinstance(value, str) and value: text_parts.append(value) if event_type in {"agent_message", "message", "response.output_text.done"}: value = event.get("text") or event.get("message") or event.get("content") if isinstance(value, str) and value: text_parts.append(value) if event_type in {"exec.completed", "turn.completed", "task.completed"}: value = event.get("last_message") or event.get("message") or event.get("output") if isinstance(value, str) and value: final_message = value comment = (final_message or "\n".join(text_parts)).strip() return session_id, comment def resolve_codex() -> str: import shutil # 1. Explicit env var exe = os.environ.get("CODEX_EXE", "") if exe and Path(exe).exists(): return exe # 2. Known install paths (checked before PATH to avoid picking up wrong codex variant) home = Path.home() for candidate in [ home / "miniconda3" / "envs" / "langgraph" / "codex.cmd", home / ".conda" / "envs" / "langgraph" / "codex.cmd", ]: if candidate.exists(): return str(candidate) # 3. PATH lookup fallback found = shutil.which("codex") or shutil.which("codex.cmd") if found: return found raise FileNotFoundError( "Cannot find codex executable. Set CODEX_EXE environment variable or add codex to PATH." ) def main() -> int: task_path = sys.argv[1] latest_discuss = sys.argv[2] if len(sys.argv) > 2 and sys.argv[2] else "" codex_session_id = sys.argv[3] if len(sys.argv) > 3 and sys.argv[3] else "" work_dir = sys.argv[4] if len(sys.argv) > 4 and sys.argv[4] else str(Path.cwd()) timeout = int(sys.argv[5]) if len(sys.argv) > 5 and sys.argv[5] else 300 prompt = ( f"Working directory: {work_dir}\n" f"Review the plan document at {task_path}.\n" "Only report severe issues.\n" "Final answer rules: if there is no severe issue, output exactly NO_SERIOUS_ISSUE.\n" "If there is a severe issue, briefly describe it in one paragraph.\n" ) if latest_discuss: prompt += f"Also verify that issues raised in {latest_discuss} were addressed.\n" codex = resolve_codex() fd, last_message_name = tempfile.mkstemp( prefix="codex-last-message-", suffix=".txt", dir=None, text=True, ) os.close(fd) last_message_path = Path(last_message_name) try: # Sandbox mode: default "read-only" -- review needs no writes, and on # Windows the config.toml "elevated" sandbox hangs every shell command # when codex runs outside the ChatGPT app (probed 2026-08-26). # Pass it with -c, NOT -s: `codex exec` accepts -s but `codex exec # resume` does not, and the second and later review passes go through # resume -- an -s here fails with a bare # For more information, try '--help' # two seconds in, on exactly the passes that matter. # (unquoted value: fails TOML parse -> codex falls back to the raw # string, which survives cmd /c quoting untouched) # # Overridable via CODEX_SANDBOX because codex's Windows sandbox cannot # start on every machine. Observed with codex-cli 0.147.0: # # execution error: Io(Custom { kind: Other, error: "windows sandbox: # orchestrator_helper_launch_canceled: ShellExecuteExW failed to # launch setup helper: 1223" }) # # 1223 is ERROR_CANCELLED -- the helper's UAC elevation prompt was # declined. The sandbox then never comes up, every shell command the # reviewer issues fails, and codex answers "Unable to read file." # # THAT FAILURE IS INDISTINGUISHABLE FROM A CLEAN REVIEW unless someone # reads the discuss file: the workflow still completes, still writes a # discuss file, and the text inside is a polite sentence rather than an # error. Same class as the known "instant NO_SERIOUS_ISSUE" trap. # On such a machine set CODEX_SANDBOX=danger-full-access. sandbox = os.environ.get("CODEX_SANDBOX", "").strip() or "read-only" flags = ["--json", "--skip-git-repo-check", "-c", "sandbox_mode=" + sandbox, "--output-last-message", str(last_message_path)] if codex_session_id: command_args = ["cmd", "/c", codex, "exec", "resume"] + flags + [codex_session_id, "-"] else: command_args = ["cmd", "/c", codex, "exec"] + flags + ["-"] logging.info("Invoking codex: %s", command_args[0] if command_args else "") t0 = time.monotonic() result = subprocess.run( command_args, capture_output=True, text=True, encoding="utf-8", errors="replace", timeout=timeout, input=prompt, cwd=work_dir, ) logging.info("Codex finished in %.1fs, exit=%d", time.monotonic() - t0, result.returncode) session_id, stream_comment = parse_jsonl(result.stdout) comment = last_message_path.read_text(encoding="utf-8").strip() if not comment: comment = stream_comment if comment.strip() == "NO_SERIOUS_ISSUE": comment = "" if codex_session_id: session_id = codex_session_id elif not session_id: session_id = codex_session_id finally: try: last_message_path.unlink() except FileNotFoundError: pass if result.returncode != 0 and not comment: sys.stderr.write(result.stderr) return result.returncode print(json.dumps( { "has_serious_issue": bool(comment), "comment": comment, "codex_session_id": session_id, }, ensure_ascii=False, )) return 0 if __name__ == "__main__": raise SystemExit(main())