#!/usr/bin/env bash
# cx.sh — the single codex (gpt-5.5) leg for run-plan, in ONE fail-closed command: review|integrate|commit.
# audience: AI coding/orchestration agents first. Invoke BY PATH; do NOT re-derive the codex exec line.
#
# WHY this exists: naive `codex exec '<prompt>'` is wrong every way an LLM would type it — hangs on stdin,
# fails/skips the git-repo check, picks codex's CONFIG-DEFAULT model+effort (not gpt-5.5), defaults to the
# READ-ONLY sandbox (a commit's merge silently no-ops), and emits free-form prose (no contract JSON, no
# exhaustion signal). One tested wrapper pins model+effort, closes stdin, sets the right sandbox per mode,
# forces strict JSON, and — critically — NEVER blocks the pipeline: ANY codex failure (quota, crash,
# malformed output) collapses to {...,"exhausted":true}, the deterministic signal run-plan uses to fall
# back to the Claude floor (opus on review/integrate, the Claude commit floor on commit).
#
# TWO USES OF CODEX, ONE CORE:
#   review|integrate — codex REASONS over a bounded diff and returns a verdict. Sandbox: read-only.
#   commit           — codex RELAYS one deterministic, idempotent run-plan-lib.sh commit (the safety
#                      envelope: refuses wrong branch, aborts on conflict, deletes nothing on failure,
#                      never touches main) and echoes its one JSON line. Sandbox: workspace-write scoped to
#                      repoRoot+intWt (a merge cannot run read-only). codex does NO cognition here.
# Everything else — arg parse, exhausted() signal, the balanced-brace verdict extractor, the bounded retry
# loop, the pinned codex invocation, the test seam — is shared. ONE source of truth, no per-mode duplicate.
#
# CONTRACT:
#   cx.sh review    --worktree DIR --base REF --head REF [--trust] [--effort high|xhigh|medium|low] [--label L]
#   cx.sh integrate --worktree DIR --base REF --head REF            [--effort ...] [--label L]
#   cx.sh commit    --repo DIR --intwt DIR --branch REF --slug S --task T --jsonl PATH [--effort ...] [--label L]
#     review:    two-dot diff BASE..HEAD  (the task's own change)
#     integrate: three-dot diff BASE...HEAD (cumulative wave change since merge-base; default-branch noise out)
#     commit:    runs `run-plan-lib.sh commit REPO INTWT BRANCH SLUG TASK JSONL` and relays its JSON.
#   - Pins: codex exec --skip-git-repo-check -m gpt-5.5 -c model_reasoning_effort=<effort>, stdin closed.
#           review/integrate add -C WORKTREE; commit adds -s workspace-write -C REPO -C INTWT.
#   - stdout: ALWAYS exactly one line of JSON, ALWAYS exit 0 when args are valid:
#       review/integrate verdict -> {"clean":bool,"findings":[{"file","issue","severity"}]}; empty diff -> clean.
#       commit verdict           -> {"committed":bool,"detail":"<...>"}
#       codex unusable           -> {...mode-shape...,"exhausted":true,"reason":"<short>"}
#   - exit 2 ONLY on caller (usage) error — bad/missing args. Never exit non-zero for a codex failure:
#     a codex failure is a VERDICT ("exhausted"), not a crash, so the caller falls back instead of halting.
#   - Model fixed at gpt-5.5; --effort selects the reasoning tier. Default: high for review/integrate
#     (real cognition), low for commit (codex only triggers the deterministic script — no reasoning needed).
#   - TEST SEAM: CX_CODEX_BIN overrides the `codex` binary (test-cx.sh injects a mock). Default: codex.
#     CX_RETRIES = total attempts (default 3); CX_RETRY_SLEEP = seconds between attempts (default 4; tests 0).
set -uo pipefail

usage() { echo "{\"clean\":false,\"findings\":[],\"exhausted\":true,\"reason\":\"usage: $*\"}" >&2; exit 2; }

CODEX_HOME_ENV=()
DEFAULT_AUTH="${HOME}/.codex/auth.json"
if [[ -e "$DEFAULT_AUTH" ]]; then
  RESOLVED_AUTH="$(readlink -f "$DEFAULT_AUTH" 2>/dev/null || true)"
  if [[ -n "$RESOLVED_AUTH" ]]; then
    RESOLVED_HOME="$(dirname "$RESOLVED_AUTH")"
    if [[ -d "$RESOLVED_HOME" ]]; then
      CODEX_HOME_ENV=(env "CODEX_HOME=$RESOLVED_HOME")
    fi
  fi
fi

MODE="${1:-}"; shift || true
case "$MODE" in review|integrate|commit|exec) : ;; *) usage "cx.sh review|integrate|commit|exec ..." ;; esac

# exec: passthrough with pinned model/effort/stdin. Takes a prompt + optional flags; strips codex-side
# overrides (-m, -c, -s) since cx.sh owns those. Output is verbatim codex stdout (no JSON contract).
# On codex failure: {"exhausted":true,"reason":"..."}.
if [[ "$MODE" == "exec" ]]; then
  _EFFORT="low" _MODEL="gpt-5.4" _WORKTREE="" _EXEC_ARGS=()
  while [[ $# -gt 0 ]]; do
    case "$1" in
      --effort)              _EFFORT="${2:-low}";    shift 2;;
      --model)               _MODEL="${2:-gpt-5.4}"; shift 2;;
      --label)               shift 2;;               # no-op in exec mode
      -C|--worktree)         _WORKTREE="${2:-}";     shift 2;;
      -m)                    _MODEL="${2:-$_MODEL}"; shift 2;;   # honor native codex model flag
      -c)                    [[ "${2:-}" =~ ^model_reasoning_effort=(.+)$ ]] && _EFFORT="${BASH_REMATCH[1]}"; shift 2;;  # honor effort; strip other -c
      -s)                    shift 2;;               # strip: cx.sh controls sandbox
      --skip-git-repo-check) shift;;                 # strip: always added below
      *)                     _EXEC_ARGS+=("$1");     shift;;
    esac
  done
  case "$_EFFORT" in high|xhigh|medium|low) : ;; *) usage "--effort must be high|xhigh|medium|low"; esac
  _CODEX_BIN="${CX_CODEX_BIN:-codex}"
  _CXARGS=(exec --skip-git-repo-check -m "$_MODEL" -c "model_reasoning_effort=$_EFFORT")
  [[ -n "$_WORKTREE" ]] && _CXARGS+=(-C "$_WORKTREE")
  _OUT="$("${CODEX_HOME_ENV[@]}" "$_CODEX_BIN" "${_CXARGS[@]}" "${_EXEC_ARGS[@]}" </dev/null 2>/dev/null)"
  _RC=$?
  if [[ $_RC -eq 0 ]]; then
    printf '%s\n' "$_OUT"
  else
    _ESC="${_MODEL//\\/\\\\}"; _ESC="${_ESC//\"/\\\"}"
    printf '{"exhausted":true,"reason":"codex rc=%d (model=%s effort=%s)"}\n' "$_RC" "$_ESC" "$_EFFORT"
  fi
  exit 0
fi

WORKTREE="" BASE="" HEAD="" TRUST="" EFFORT="" LABEL="" MODEL="gpt-5.4"
REPO="" INTWT="" BRANCH="" SLUG="" TASK="" JSONL=""
while [[ $# -gt 0 ]]; do
  case "$1" in
    --worktree) WORKTREE="${2:-}"; shift 2;;
    --base)     BASE="${2:-}";     shift 2;;
    --head)     HEAD="${2:-}";     shift 2;;
    --trust)    TRUST="1";         shift 1;;
    --repo)     REPO="${2:-}";     shift 2;;
    --intwt)    INTWT="${2:-}";    shift 2;;
    --branch)   BRANCH="${2:-}";   shift 2;;
    --slug)     SLUG="${2:-}";     shift 2;;
    --task)     TASK="${2:-}";     shift 2;;
    --jsonl)    JSONL="${2:-}";    shift 2;;
    --effort)   EFFORT="${2:-}";   shift 2;;
    --model)    MODEL="${2:-}";    shift 2;;
    --label)    LABEL="${2:-}";    shift 2;;
    *) shift;;
  esac
done

# per-mode required args + effort default (all modes: default low; override via --effort).
if [[ "$MODE" == "commit" ]]; then
  [[ -n "$REPO"   && -d "$REPO"  ]] || usage "--repo missing or not a dir"
  [[ -n "$INTWT"  && -d "$INTWT" ]] || usage "--intwt missing or not a dir"
  [[ -n "$BRANCH" ]] || usage "--branch REF required"
  [[ -n "$SLUG"   ]] || usage "--slug S required"
  [[ -n "$TASK"   ]] || usage "--task T required"
  EFFORT="${EFFORT:-low}"
else
  [[ -n "$WORKTREE" && -d "$WORKTREE" ]] || usage "--worktree missing or not a dir"
  [[ -n "$BASE" ]] || usage "--base REF required"
  [[ -n "$HEAD" ]] || usage "--head REF required"
  EFFORT="${EFFORT:-low}"
fi
case "$EFFORT" in high|xhigh|medium|low) : ;; *) usage "--effort must be high|xhigh|medium|low" ;; esac
CODEX_BIN="${CX_CODEX_BIN:-codex}"

# exhausted verdict — the deterministic fallback signal. Shape matches the mode's contract. Reason logged, never trusted as a pass.
exhausted() {
  local reason; reason="$(printf '%s' "${1:-codex-unavailable}" | tr -d '\n' | cut -c1-200)"
  reason="${reason//\\/\\\\}"; reason="${reason//\"/\\\"}"
  if [[ "$MODE" == "commit" ]]; then
    printf '{"committed":false,"detail":"%s","exhausted":true,"reason":"%s"}\n' "$reason" "$reason"
  else
    printf '{"clean":false,"findings":[],"exhausted":true,"reason":"%s"}\n' "$reason"
  fi
  exit 0
}

# build the per-mode PROMPT + codex sandbox flags. review/integrate embed a bounded diff and ask codex to
# reason; commit embeds the ONE deterministic command and asks codex to relay its JSON verbatim.
CODEX_ARGS=(exec --skip-git-repo-check)
if [[ "$MODE" == "commit" ]]; then
  CODEX_ARGS+=(-s workspace-write -C "$REPO" --add-dir "$INTWT")
  # args are repo-internal paths embedded into a prompt codex runs verbatim (no untrusted input).
  COMMIT_CMD="bash ~/.claude/workflows/lib/run-plan-lib.sh commit \"$REPO\" \"$INTWT\" \"$BRANCH\" \"$SLUG\" \"$TASK\" \"$JSONL\""
  read -r -d '' PROMPT <<EOF || true
Run EXACTLY this one shell command and nothing else. Do NOT inspect the repo, do NOT reformulate, do NOT run any other command:
  $COMMIT_CMD
The command prints exactly one line of JSON: {"committed":<bool>,"detail":"<...>"}.
OUTPUT CONTRACT — output ONLY that single JSON line verbatim, no markdown fences, no prose before or after.
EOF
else
  CODEX_ARGS+=(-C "$WORKTREE")
  DIFF_RANGE="$BASE..$HEAD"; [[ "$MODE" == "integrate" ]] && DIFF_RANGE="$BASE...$HEAD"
  DIFF="$(git -C "$WORKTREE" diff "$DIFF_RANGE" 2>/dev/null)" || { printf '{"clean":false,"findings":[{"file":"worktree","issue":"git diff failed for %s — refs not reachable in worktree","severity":"blocker"}]}\n' "$DIFF_RANGE"; exit 0; }
  [[ -n "$DIFF" ]] || { printf '{"clean":true,"findings":[]}\n'; exit 0; }
  # Cap embedded diff; codex still has -C WORKTREE to open any file it needs beyond the cap.
  DIFF_CAP=60000
  if [[ ${#DIFF} -gt $DIFF_CAP ]]; then
    DIFF="${DIFF:0:$DIFF_CAP}
[... diff truncated at ${DIFF_CAP} chars — open files under the worktree for the rest ...]"
  fi
  SCOPE="HIGH-risk code review of this diff"; RANGE_DESC="the task's change ($BASE..$HEAD)"
  if [[ "$MODE" == "integrate" ]]; then
    SCOPE="integration review of the COMBINED wave diff"; RANGE_DESC="cumulative wave change ($BASE...$HEAD)"
  fi
  TRUST_LINE=""; [[ -n "$TRUST" ]] && TRUST_LINE=" TRUST BOUNDARY touched — scrutinize the boundary hard."
  read -r -d '' PROMPT <<EOF || true
You are a senior security+correctness reviewer. Perform an adversarial ${SCOPE} (${RANGE_DESC}).
Hunt, in priority order: correctness bugs, money/auth defects, IDOR/missing-authorization, injection,
fail-open paths, atomicity/idempotency, cross-task contract breaks.${TRUST_LINE}
WARNING-SUPPRESSION CHECK: if the diff adds lines to a .warnignore, treat each new entry adversarially.
A suppression is legitimate ONLY for a genuinely-unfixable warning (third-party/upstream, un-bumpable dep).
If a newly-suppressed warning is from THIS diff's own code or is fixable, that is a blocker finding:
demand the warning be FIXED and the entry removed. A vague justification ("benign","noise") is itself a blocker.

OUTPUT CONTRACT — output ONLY a single minified JSON object, no markdown fences, no prose before or after:
{"clean":<true only if ZERO must-fix findings>,"findings":[{"file":"<path>","issue":"<one line>","severity":"<critical|high|medium|low>"}]}
If there are no must-fix findings, output exactly {"clean":true,"findings":[]}.

=== DIFF (${DIFF_RANGE}) ===
${DIFF}
EOF
fi

# extract + validate the verdict from one codex run. codex output may carry stray prose; pull the LAST
# balanced object satisfying the mode's shape (review: clean:bool+findings:list; commit: committed:bool).
# Empty/none => caller treats this attempt as transient and retries.
extract_verdict() {
  CX_OUT="$1" CX_MODE="$MODE" python3 - <<'PY' 2>/dev/null
import os, sys, json, re
raw = os.environ.get('CX_OUT', '')
mode = os.environ.get('CX_MODE', 'review')
raw = re.sub(r'```(?:json)?', '', raw)
cands = []
for m in re.finditer(r'\{', raw):
    depth = 0
    for i in range(m.start(), len(raw)):
        c = raw[i]
        if c == '{': depth += 1
        elif c == '}':
            depth -= 1
            if depth == 0:
                cands.append(raw[m.start():i+1]); break
obj = None
for c in reversed(cands):
    try: o = json.loads(c)
    except Exception: continue
    if mode == 'commit':
        if isinstance(o, dict) and isinstance(o.get('committed'), bool):
            obj = o; break
    else:
        if isinstance(o, dict) and isinstance(o.get('clean'), bool) and isinstance(o.get('findings'), list):
            obj = o; break
if obj is None:
    sys.exit(1)
if mode == 'commit':
    print(json.dumps({'committed': obj['committed'], 'detail': str(obj.get('detail', ''))}, separators=(',', ':')))
else:
    norm = []
    for f in obj['findings']:
        if not isinstance(f, dict): sys.exit(1)
        norm.append({
            'file': str(f.get('file', '')),
            'issue': str(f.get('issue', '')),
            'severity': str(f.get('severity', '')),
        })
    print(json.dumps({'clean': obj['clean'], 'findings': norm}, separators=(',', ':')))
PY
}

# run codex, pinned, with BOUNDED RETRY. A non-zero rc OR a malformed verdict is most often TRANSIENT
# (network blip, brief 429, model hiccup); a retry recovers it. A real hard wall persists across every
# attempt and still collapses to exhausted. Retrying empirically is STRICTLY more robust than
# string-classifying the failure (the old single-shot grep'd stderr for "rate limit|429" and mis-tagged
# unrelated errors as a limit). We retry, and report the REAL accumulated reason only if every attempt fails.
ERR="$(mktemp)"; trap 'rm -f "$ERR"' EXIT
RETRIES="${CX_RETRIES:-3}"; case "$RETRIES" in ''|*[!0-9]*) RETRIES=3;; esac; [[ "$RETRIES" -lt 1 ]] && RETRIES=1
RSLEEP="${CX_RETRY_SLEEP:-4}"; case "$RSLEEP" in ''|*[!0-9]*) RSLEEP=4;; esac

LASTREASON=""
for ((attempt=1; attempt<=RETRIES; attempt++)); do
  OUT="$("${CODEX_HOME_ENV[@]}" "$CODEX_BIN" "${CODEX_ARGS[@]}" -m "$MODEL" -c model_reasoning_effort="$EFFORT" "$PROMPT" </dev/null 2>"$ERR")"
  RC=$?
  if [[ $RC -ne 0 ]]; then
    ETAIL="$(tail -3 "$ERR" 2>/dev/null | tr '\n' ' ')"
    LASTREASON="codex rc=$RC: $ETAIL"
  else
    VERDICT="$(extract_verdict "$OUT")"
    if [[ -n "$VERDICT" ]]; then printf '%s\n' "$VERDICT"; exit 0; fi
    LASTREASON="codex output not valid ${MODE} json"
  fi
  [[ $attempt -lt $RETRIES && "$RSLEEP" -gt 0 ]] && sleep "$RSLEEP" 2>/dev/null
done
# every attempt failed — fall back to the Claude floor, with the real reason.
exhausted "${LASTREASON:-codex-unavailable} (after $RETRIES attempts)"
