#!/usr/bin/env bash
# Bash 5.3+ can deadlock writing heredoc pipes on macOS before the reader starts.
if [[ ${OSTYPE:-} == darwin* && $BASH != /bin/bash ]] && ((BASH_VERSINFO[0] > 5 || (BASH_VERSINFO[0] == 5 && BASH_VERSINFO[1] >= 3))); then
  exec /bin/bash "$0" "$@"
fi

set -euo pipefail

# This wrapper parses GitHub CLI JSON. Caller shells may force ANSI color globally.
export NO_COLOR=1
export CLICOLOR=0
export CLICOLOR_FORCE=0
export FORCE_COLOR=0
unset COLORTERM

# This is the single source of truth for the canonical-wrapper trust boundary.
# Advisory commands may run a mismatched local wrapper only with the explicit
# developer opt-in; landing commands must always use canonical/origin-main code.
# Classification is independent of serialization: ci-dispatch remains locked
# because GitHub exposes neither dispatch deduplication nor a correlation ID.
# PR_SUBCOMMAND_CLASSIFICATIONS_BEGIN
pr_subcommand_classification() {
  case "$1" in
    ls | ci-dispatch)
      printf 'advisory\n'
      ;;
    gc | lock-recover | review-init | review-checkout-main | review-checkout-pr | review-claim | review-guard | review-artifacts-init | review-validate-artifacts | review-tests | prepare-init | prepare-validate-commit | prepare-gates | prepare-push | prepare-sync-head | prepare-run | merge-verify | merge-run | merge-recover | merge-complete)
      printf 'landing\n'
      ;;
    *) return 1 ;;
  esac
}
# PR_SUBCOMMAND_CLASSIFICATIONS_END

dev_wrapper_opt_in=0
if [ "${OPENCLAW_PR_DEV_WRAPPER:-}" = "1" ]; then
  dev_wrapper_opt_in=1
fi
if [ "${1-}" = "--dev-wrapper" ]; then
  dev_wrapper_opt_in=1
  export OPENCLAW_PR_DEV_WRAPPER=1
  shift
fi
requested_subcommand="${1-}"

# Select trusted wrapper code independently from the canonical repository root;
# a linked wrapper may be removed by merge-run or gc before supervision ends.
# Physical paths keep helper argv aligned with Node's canonical module URLs.
script_self="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)/$(basename "${BASH_SOURCE[0]}")"
script_parent_dir="$(dirname "$script_self")"
canonical_repo_root="$script_parent_dir/.."
# Bootstrap paths stay fixed so an edited inventory cannot exclude itself from
# the working-tree and anchor checks. The inventory owns the remaining closure.
pr_wrapper_components=(scripts/pr scripts/pr-lib)
# Anchor-exec handoff: this process was exec'd from wrapper bytes materialized
# out of refs/remotes/origin/main by the selection logic below in a previous
# wrapper. The env var conveys repository addressing only — code trust came
# from the parent's per-blob verification of the materialized copy, the same
# verify-then-exec contract as the canonical-checkout substitution. Skip
# re-selection: the running bytes ARE the anchor, and the temp copy has no git
# context of its own to select against.
if [ -n "${OPENCLAW_PR_ANCHOR_REPO_ROOT:-}" ]; then
  if ! git -C "$OPENCLAW_PR_ANCHOR_REPO_ROOT" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
    echo "OPENCLAW_PR_ANCHOR_REPO_ROOT is not a git work tree: $OPENCLAW_PR_ANCHOR_REPO_ROOT" >&2
    exit 1
  fi
  canonical_repo_root="$OPENCLAW_PR_ANCHOR_REPO_ROOT"
elif common_git_dir=$(git -C "$script_parent_dir" rev-parse --path-format=absolute --git-common-dir 2>/dev/null); then
  canonical_repo_root="$(dirname "$common_git_dir")"
  canonical_self="$canonical_repo_root/scripts/$(basename "${BASH_SOURCE[0]}")"
  if [ "$script_self" != "$canonical_self" ] && [ -x "$canonical_self" ]; then
    linked_inventory=$(git -C "$script_parent_dir" show HEAD:scripts/pr-lib/wrapper-components.txt)
    while IFS= read -r wrapper_component; do
      pr_wrapper_components+=("$wrapper_component")
    done <<< "$linked_inventory"
    if ! git -C "$script_parent_dir" diff --quiet HEAD -- "${pr_wrapper_components[@]/#/:(top)}"; then
      echo "scripts/pr wrapper files have uncommitted changes in this worktree." >&2
      echo "Refusing to run unreviewed wrapper code from: $script_parent_dir" >&2
      exit 1
    fi
    linked_wrapper_revision=$(
      git -C "$script_parent_dir" rev-parse "${pr_wrapper_components[@]/#/HEAD:}" 2>/dev/null || true
    )
    canonical_wrapper_revision=$(
      git -C "$canonical_repo_root" rev-parse "${pr_wrapper_components[@]/#/HEAD:}" 2>/dev/null || true
    )
    if [ -n "$linked_wrapper_revision" ] &&
      [ "$linked_wrapper_revision" = "$canonical_wrapper_revision" ] &&
      git -C "$canonical_repo_root" diff --quiet HEAD -- "${pr_wrapper_components[@]/#/:(top)}"; then
      exec "$canonical_self" "$@"
    fi
    # The canonical checkout can be parked on another branch or carry local
    # edits (release trains move it); maintainer-controlled origin/main is the
    # trust anchor then. Run THIS worktree's committed wrapper, without
    # substitution, when it exactly matches origin/main.
    # refs/remotes/... explicitly: bare "origin/main" is a DWIM name that a
    # local branch or tag named origin/main could shadow, spoofing the anchor.
    anchor_commit=$(git -C "$script_parent_dir" rev-parse --verify refs/remotes/origin/main^{commit} 2>/dev/null || true)
    anchor_wrapper_revision=""
    if [ -n "$anchor_commit" ] &&
      anchor_inventory=$(git -C "$script_parent_dir" show "$anchor_commit:scripts/pr-lib/wrapper-components.txt" 2>/dev/null); then
      # A newer anchor can add helpers outside the caller's inventory. Its own
      # closure governs canonical substitution as well as extraction below.
      anchor_components=(scripts/pr scripts/pr-lib)
      while IFS= read -r anchor_component; do
        anchor_components+=("$anchor_component")
      done <<< "$anchor_inventory"
      anchor_wrapper_revision=$(
        git -C "$script_parent_dir" rev-parse "${anchor_components[@]/#/$anchor_commit:}" 2>/dev/null || true
      )
      canonical_wrapper_revision=$(
        git -C "$canonical_repo_root" rev-parse "${anchor_components[@]/#/HEAD:}" 2>/dev/null || true
      )
    fi
    if [ -z "$linked_wrapper_revision" ] ||
      [ -z "$anchor_wrapper_revision" ] ||
      [ "$linked_wrapper_revision" != "$anchor_wrapper_revision" ]; then
      requested_classification=$(pr_subcommand_classification "$requested_subcommand" 2>/dev/null || true)
      if [ "$dev_wrapper_opt_in" = "1" ] && [ "$requested_classification" = "advisory" ]; then
        if [ "${OPENCLAW_PR_DEV_WRAPPER_BANNER_SHOWN:-}" != "1" ]; then
          local_head_revision=$(git -C "$script_parent_dir" rev-parse HEAD 2>/dev/null || printf 'unknown')
          echo "WARNING: running local scripts/pr revision $local_head_revision via dev-wrapper opt-in." >&2
          echo "subcommand '$requested_subcommand' is classified advisory." >&2
          echo "The local wrapper differs from the canonical checkout and origin/main; landing subcommands remain refused." >&2
          export OPENCLAW_PR_DEV_WRAPPER_BANNER_SHOWN=1
        fi
      else
        if [ "$dev_wrapper_opt_in" = "1" ] && [ -n "$requested_classification" ]; then
          echo "subcommand '$requested_subcommand' is classified $requested_classification; dev-wrapper opt-in is unavailable." >&2
        fi
        # Worktrees routinely sit on a base that predates (or carries) wrapper
        # changes relative to main. When the canonical checkout is byte-identical
        # to the fetched origin/main anchor, exec-ing it runs exactly the trusted
        # anchor code; announce the substitution so it is never silent.
        if [ -n "$anchor_wrapper_revision" ] &&
          [ "$canonical_wrapper_revision" = "$anchor_wrapper_revision" ] &&
          git -C "$canonical_repo_root" diff --quiet HEAD -- "${anchor_components[@]/#/:(top)}"; then
          echo "scripts/pr wrapper in this worktree differs from origin/main; running the canonical checkout's wrapper (matches the origin/main trust anchor): $canonical_repo_root" >&2
          exec "$canonical_self" "$@"
        fi
        # Neither this worktree nor the canonical checkout matches the fetched
        # origin/main anchor. Materialize the anchor wrapper bytes directly
        # from refs/remotes/origin/main and exec that copy: trust flows from
        # the fetched ref itself, so a queued PR stops paying a rebase + CI lap
        # for unrelated wrapper drift on main. Requires the anchor entrypoint
        # to understand the handoff; older anchors fall through to the refusal.
        # Archive emits paths relative to its cwd, so run it (and the
        # verification listing) from the worktree toplevel to keep component
        # paths intact in the materialized tree.
        anchor_git_root=$(git -C "$script_parent_dir" rev-parse --show-toplevel 2>/dev/null || true)
        if [ -n "$anchor_wrapper_revision" ] && [ -n "$anchor_git_root" ] &&
          anchor_exec_dir=$(mktemp -d "${TMPDIR:-/tmp}/openclaw-pr-anchor.XXXXXX" 2>/dev/null); then
          anchor_extraction_valid=0
          # BSD tar can stop at the end marker before Git finishes writing padding,
          # causing SIGPIPE under pipefail. Require producer completion before reading.
          if git -C "$anchor_git_root" archive "$anchor_commit" -- \
            "${anchor_components[@]}" > "$anchor_exec_dir/anchor.tar" 2>/dev/null &&
            tar -xf "$anchor_exec_dir/anchor.tar" -C "$anchor_exec_dir" 2>/dev/null; then
            rm "$anchor_exec_dir/anchor.tar"
            anchor_extraction_valid=1
            anchor_verified_entries=0
            # Verify every extracted file byte-matches its anchor blob so an
            # archive/extract fault can never smuggle different code past the
            # trust boundary. The temp dir is 0700 and exec'd immediately —
            # the same TOCTOU class as the canonical-substitution exec above.
            while IFS=$'\t' read -r anchor_meta anchor_path; do
              anchor_blob="${anchor_meta##* }"
              extracted_blob=$(git -C "$anchor_git_root" hash-object -- \
                "$anchor_exec_dir/$anchor_path" 2>/dev/null || true)
              if [ -z "$extracted_blob" ] || [ "$extracted_blob" != "$anchor_blob" ]; then
                anchor_extraction_valid=0
                break
              fi
              anchor_verified_entries=$((anchor_verified_entries + 1))
            done < <(git -C "$anchor_git_root" ls-tree -r --full-tree "$anchor_commit" -- "${anchor_components[@]}")
            # An empty listing means the anchor read failed; never exec unverified bytes.
            if [ "$anchor_verified_entries" = "0" ]; then
              anchor_extraction_valid=0
            fi
          fi
          if [ "$anchor_extraction_valid" = "1" ] &&
            grep -q "OPENCLAW_PR_ANCHOR_REPO_ROOT" "$anchor_exec_dir/scripts/pr" 2>/dev/null; then
            # Only third-party tooling comes from the installed canonical tree.
            # Pin package directories: a later install may replace top-level aliases.
            # A whole node_modules link could load unanchored workspace source.
            if ! node --input-type=module - "$canonical_repo_root/node_modules" "$anchor_exec_dir/node_modules" <<'EOF_NODE'
import { mkdirSync, realpathSync, statSync, symlinkSync } from "node:fs";
import { join } from "node:path";
const dependencies = ["tsx", "zod", "minimatch", "yaml"].map((dependency) => {
  const installedPath = join(process.argv[2], dependency);
  try {
    const target = realpathSync(installedPath);
    if (!statSync(target).isDirectory()) throw new Error("not a package directory");
    return { dependency, target };
  } catch {
    console.error(`Cannot resolve installed scripts/pr dependency '${dependency}' at ${installedPath}.`);
    console.error("Restore frozen dependencies in a clean trusted-main checkout before retrying; no dependencies were installed.");
    process.exit(1);
  }
});
mkdirSync(process.argv[3]);
for (const { dependency, target } of dependencies) {
  // Native junctions avoid Git Bash copying package directories on Windows.
  symlinkSync(target, join(process.argv[3], dependency),
    process.platform === "win32" ? "junction" : "dir");
}
EOF_NODE
            then
              rm -rf "$anchor_exec_dir"
              exit 1
            fi
            # Not cleaned by trap: exec replaces this shell, and bash re-reads
            # the script file during execution, so the copy must outlive the
            # whole supervised run. OS tmp reaping owns this directory.
            echo "scripts/pr wrapper in this worktree differs from origin/main; running wrapper code materialized from the refs/remotes/origin/main trust anchor at revision $(git -C "$script_parent_dir" rev-parse --short "$anchor_commit")." >&2
            OPENCLAW_PR_ANCHOR_REPO_ROOT="$canonical_repo_root" exec "$anchor_exec_dir/scripts/pr" "$@"
          fi
          rm -rf "$anchor_exec_dir"
        fi
        # HEAD blobs are authoritative here: the uncommitted-wrapper guard above
        # already exited for any staged or unstaged edit to these paths, so
        # the working tree matches HEAD and this list matches what was rejected.
        differing_wrapper_components=()
        for wrapper_component in "${pr_wrapper_components[@]}"; do
          linked_component_revision=$(git -C "$script_parent_dir" rev-parse "HEAD:$wrapper_component" 2>/dev/null || true)
          anchor_component_revision=$(git -C "$script_parent_dir" rev-parse "refs/remotes/origin/main:$wrapper_component" 2>/dev/null || true)
          if [ -z "$linked_component_revision" ] ||
            [ -z "$anchor_component_revision" ] ||
            [ "$linked_component_revision" != "$anchor_component_revision" ]; then
            differing_wrapper_components+=("$wrapper_component")
          fi
        done
        echo "scripts/pr implementation differs between this worktree and the canonical checkout, and does not match origin/main." >&2
        echo "differing wrapper components vs origin/main: ${differing_wrapper_components[*]}" >&2
        echo "Refusing to silently substitute canonical wrapper code from: $canonical_repo_root" >&2
        echo "Run scripts/pr from a checkout whose wrapper matches the canonical checkout or a fetched origin/main." >&2
        exit 1
      fi
    fi
  fi
fi

is_locked_pr_command() {
  # Advisory trust classification permits local dogfood, but dispatches still
  # serialize with landing operations because the remote mutation is not atomic.
  if [ "$1" = "ci-dispatch" ]; then
    return 0
  fi
  local classification
  classification=$(pr_subcommand_classification "$1") || return 1
  [ "$classification" = "landing" ] || return 1
  # gc manages per-PR locks itself; lock-recover performs an exact-OID CAS.
  [ "$1" != "gc" ] && [ "$1" != "lock-recover" ]
}

is_main_only_pr_command() {
  case "$1" in
    prepare-init | prepare-validate-commit | prepare-gates | prepare-push | prepare-sync-head | prepare-run | merge-verify | merge-run | merge-recover) return 0 ;;
    *) return 1 ;;
  esac
}

is_supervised_pr_process() {
  [ "${OPENCLAW_PR_DEDICATED_PROCESS_GROUP:-}" = "1" ] &&
    [ "${OPENCLAW_PR_LOCK_NOTIFY_FD:-}" = "3" ] &&
    [ "${OPENCLAW_PR_LOCK_SUPERVISOR_PID:-}" = "$PPID" ]
}

# The supervisor changes cwd to the canonical repo. Resolve an explicit body
# relative to the operator's caller before crossing that boundary; do not read it.
if [ "${1-}" = merge-run ] || [ "${1-}" = merge-recover ]; then
  merge_cli_args=() merge_cli_body_next=false
  for merge_cli_arg in "$@"; do
    if [ "$merge_cli_body_next" = true ] && [ -n "$merge_cli_arg" ]; then
      merge_cli_arg=$(node -e 'process.stdout.write(require("node:path").resolve(process.argv[1]))' -- "$merge_cli_arg") || exit 1
    fi
    merge_cli_args+=("$merge_cli_arg")
    merge_cli_body_next=false
    [ "$merge_cli_arg" != --body-file ] || merge_cli_body_next=true
  done
  set -- "${merge_cli_args[@]}"
fi

if [ "${1-}" = "gc" ] || is_locked_pr_command "${1-}"; then
  if is_supervised_pr_process; then
    # operation-lock.sh consumes the one-shot marker when it installs the
    # leader-only completion trap, before PR command tools can inherit it.
    :
  else
    unset OPENCLAW_PR_DEDICATED_PROCESS_GROUP
    unset OPENCLAW_PR_LOCK_NOTIFY_FD
    unset OPENCLAW_PR_LOCK_SUPERVISOR_PID
    command -v node >/dev/null 2>&1 || { echo "Missing required command: node" >&2; exit 1; }
    exec node "$script_parent_dir/pr-lib/process-group-runner.mjs" "$canonical_repo_root" "$script_self" "$@"
  fi
fi

# shellcheck disable=SC1091
source "$script_parent_dir/lib/plain-gh.sh"

usage() {
  cat <<USAGE
Usage:
  scripts/pr [--dev-wrapper] <subcommand> ...
  scripts/pr ls
  scripts/pr gc [--dry-run]
  scripts/pr lock-recover <PR> <OWNER_OID> --confirmed-no-running-tools
  scripts/pr review-init <PR>
  scripts/pr review-checkout-main <PR>
  scripts/pr review-checkout-pr <PR>
  scripts/pr review-claim <PR>
  scripts/pr review-guard <PR>
  scripts/pr review-artifacts-init <PR>
  scripts/pr review-validate-artifacts <PR>
  scripts/pr review-tests <PR> <test-file> [<test-file> ...]
  scripts/pr prepare-init <PR>
  scripts/pr prepare-validate-commit <PR>
  scripts/pr prepare-gates <PR>
  scripts/pr prepare-push <PR>
  scripts/pr prepare-sync-head <PR>
  scripts/pr prepare-run <PR>
  scripts/pr ci-dispatch <PR> [--backend crabbox --run-id <id> --lease-id <id> --bootstrap-sha256 <hash>]
  scripts/pr merge-verify <PR>
  scripts/pr merge-run <PR> [--auto-merge] [--body-file <path>]
    OPENCLAW_PR_MERGE_METHOD=merge|rebase preserves the PR commit series.
    --auto-merge selects pinned immediate squash for MERGEABLE/CLEAN, auto for MERGEABLE/BEHIND.
    OPENCLAW_PR_AUTO_MERGE=1 is equivalent.
    --body-file snapshots a regular UTF-8 file relative to the caller, replacing squash prose.
    Empty files are valid. Explicit/source co-authors and preview credit backed by PR commits are retained; known machine credit is excluded. Queue merges reject it.
    Repeated merge-run reconciles retained outcomes; it never retries an uncertain dispatch.
  scripts/pr merge-recover <PR> <OUTCOME_OID> --confirmed-operator-recovery [--replacement-head <SHA>] [--body-file <path>]
    Explicitly authorize one new attempt after inspecting the retained outcome and remote history.
    Defaults to the retained head; replacement requires an explicitly approved full lowercase 40-character SHA,
    freshly authored exact-head review/preparation and completed CI proof. Never rebases automatically.
    Keeps prior evidence and reruns admission; requires the same method and no auto/queue/admin route.
  scripts/pr merge-complete <PR> <OUTCOME_OID> --confirmed-operator-completion
    Finish a verified merge receipt after cleanup; never dispatches a merge or deletes resources.
    Posts a first completion comment only from merged; uncertain comment attempts are lookup-only.

  --dev-wrapper permits a mismatched local wrapper only for subcommands
  classified advisory. OPENCLAW_PR_DEV_WRAPPER=1 is equivalent.

Required commands: git, gh, jq, rg (ripgrep), pnpm, node.
USAGE
}

require_cmds() {
  local missing=()
  local cmd
  for cmd in git gh jq rg pnpm node; do
    if ! command -v "$cmd" >/dev/null 2>&1; then
      missing+=("$cmd")
    fi
  done
  if ! resolve_plain_gh_bin >/dev/null; then
    missing+=("gh")
  fi

  if [ "${#missing[@]}" -gt 0 ]; then
    echo "Missing required command(s): ${missing[*]}" >&2
    if [[ " ${missing[*]} " = *" rg "* ]]; then
      echo "Install ripgrep and retry: https://github.com/BurntSushi/ripgrep#installation" >&2
    fi
    exit 1
  fi
}

require_main_target_pr() {
  local pr="$1"
  local base base_json
  base_json=$(read_pr_view_json "$pr" "baseRefName") || exit 1
  base=$(pr_view_string_field "$base_json" "baseRefName" "$pr" "Retry the scripts/pr command.") || exit 1
  if [ "$base" != "main" ]; then
    echo "scripts/pr prepare and merge commands only support PRs targeting main; PR #$pr targets $base." >&2
    echo "Use the reviewed release-branch landing flow for non-main PRs." >&2
    exit 1
  fi
}

# shellcheck disable=SC1091
source "$script_parent_dir/pr-lib/worktree.sh"
# shellcheck disable=SC1091
source "$script_parent_dir/pr-lib/operation-lock.sh"
# shellcheck disable=SC1091
source "$script_parent_dir/pr-lib/common.sh"
# shellcheck disable=SC1091
source "$script_parent_dir/pr-lib/changelog.sh"
# shellcheck disable=SC1091
source "$script_parent_dir/pr-lib/gates.sh"
# shellcheck disable=SC1091
source "$script_parent_dir/pr-lib/push.sh"
# shellcheck disable=SC1091
source "$script_parent_dir/pr-lib/review.sh"
# shellcheck disable=SC1091
source "$script_parent_dir/pr-lib/prepare-core.sh"
# shellcheck disable=SC1091
source "$script_parent_dir/pr-lib/merge.sh"

main() {
  if [ "$#" -lt 1 ]; then
    usage
    exit 2
  fi

  local cmd="${1-}"
  shift || true

  if [ "$cmd" = "lock-recover" ]; then
    local pr="${1-}"
    local owner_oid="${2-}"
    local confirmation="${3-}"
    [ -n "$pr" ] && [ -n "$owner_oid" ] && [ "$#" -eq 3 ] || { usage; exit 2; }
    recover_pr_operation_lock "$pr" "$owner_oid" "$confirmation"
    return
  fi

  case "$cmd" in
    ls) ;;
    gc)
      [ "$#" -eq 0 ] || { [ "$#" -eq 1 ] && [ "$1" = "--dry-run" ]; } || {
        usage
        exit 2
      }
      ;;
    review-tests)
      [ "$#" -ge 2 ] || { usage; exit 2; }
      ;;
    merge-complete)
      [ "$#" -eq 3 ] && is_canonical_pr_number "$1" &&
        [[ "$2" =~ ^[0-9a-f]{40}$ ]] && [ "$3" = --confirmed-operator-completion ] || { usage; exit 2; }
      ;;
    merge-run | merge-recover)
      local merge_pr="${1-}" auto_merge=false recovery_oid="" replacement_head="" body_path=""
      [ -n "$merge_pr" ] || { usage; exit 2; }
      shift
      if [ "$cmd" = merge-recover ]; then
        [ "$#" -ge 2 ] && is_canonical_pr_number "$merge_pr" &&
          [[ "$1" =~ ^[0-9a-f]{40}$ ]] && [ "$2" = --confirmed-operator-recovery ] || { usage; exit 2; }
        recovery_oid="$1"
        shift 2
      fi
      while [ "$#" -gt 0 ]; do
        case "$1" in
          --auto-merge)
            [ "$cmd" = merge-run ] && [ "$auto_merge" = false ] || { usage; exit 2; }
            auto_merge=true
            shift
            ;;
          --body-file)
            [ "$#" -ge 2 ] && [ -n "$2" ] && [ -z "$body_path" ] || { usage; exit 2; }
            body_path="$2"
            shift 2
            ;;
          --replacement-head)
            [ "$cmd" = merge-recover ] && [ "$#" -ge 2 ] &&
              [[ "$2" =~ ^[0-9a-f]{40}$ ]] && [ -z "$replacement_head" ] || { usage; exit 2; }
            replacement_head="$2"
            shift 2
            ;;
          *) usage; exit 2 ;;
        esac
      done
      if [ "$cmd" = merge-run ] && [ "${OPENCLAW_PR_AUTO_MERGE:-}" = 1 ]; then
        auto_merge=true
      fi
      set -- "$merge_pr"
      ;;
    review-init | review-checkout-main | review-checkout-pr | review-claim | review-guard | review-artifacts-init | review-validate-artifacts | prepare-init | prepare-validate-commit | prepare-gates | prepare-push | prepare-sync-head | prepare-run | ci-dispatch | merge-verify)
      [ "$#" -ge 1 ] || { usage; exit 2; }
      ;;
    *)
      usage
      exit 2
      ;;
  esac

  require_cmds

  # merge-run reconciles retained outcomes before reading disposable artifacts or base guards.
  if [ "$cmd" != merge-run ] && is_main_only_pr_command "$cmd"; then
    require_main_target_pr "${1-}"
  fi

  if is_locked_pr_command "$cmd"; then
    local locked_pr="${1-}"
    acquire_pr_operation_lock "$locked_pr"
    begin_pr_operation_validation_phase
    # Temp-backed shell redirections must fail while the supervisor can auto-release.
    validate_pr_temp_storage
    trap 'exit 129' HUP
    trap 'exit 130' INT
    trap 'exit 131' QUIT
    trap 'exit 143' TERM
  fi

  case "$cmd" in
    ls)
      list_pr_worktrees
      ;;
    gc)
      local dry_run=false
      if [ "$#" -eq 1 ]; then
        dry_run=true
      fi
      gc_pr_worktrees "$dry_run"
      ;;
    review-init)
      local pr="${1-}"
      [ -n "$pr" ] || { usage; exit 2; }
      review_init "$pr"
      ;;
    review-checkout-main)
      local pr="${1-}"
      [ -n "$pr" ] || { usage; exit 2; }
      review_checkout_main "$pr"
      ;;
    review-checkout-pr)
      local pr="${1-}"
      [ -n "$pr" ] || { usage; exit 2; }
      review_checkout_pr "$pr"
      ;;
    review-claim)
      local pr="${1-}"
      [ -n "$pr" ] || { usage; exit 2; }
      review_claim "$pr"
      ;;
    review-guard)
      local pr="${1-}"
      [ -n "$pr" ] || { usage; exit 2; }
      review_guard "$pr"
      ;;
    review-artifacts-init)
      local pr="${1-}"
      [ -n "$pr" ] || { usage; exit 2; }
      review_artifacts_init "$pr"
      ;;
    review-validate-artifacts)
      local pr="${1-}"
      [ -n "$pr" ] || { usage; exit 2; }
      review_validate_artifacts "$pr"
      ;;
    review-tests)
      local pr="${1-}"
      [ -n "$pr" ] || { usage; exit 2; }
      shift || true
      review_tests "$pr" "$@"
      ;;
    prepare-init)
      local pr="${1-}"
      [ -n "$pr" ] || { usage; exit 2; }
      prepare_init "$pr"
      ;;
    prepare-validate-commit)
      local pr="${1-}"
      [ -n "$pr" ] || { usage; exit 2; }
      prepare_validate_commit "$pr"
      ;;
    prepare-gates)
      local pr="${1-}"
      [ -n "$pr" ] || { usage; exit 2; }
      prepare_gates "$pr"
      ;;
    prepare-push)
      local pr="${1-}"
      [ -n "$pr" ] || { usage; exit 2; }
      prepare_push "$pr"
      ;;
    prepare-sync-head)
      local pr="${1-}"
      [ -n "$pr" ] || { usage; exit 2; }
      prepare_sync_head "$pr"
      ;;
    prepare-run)
      local pr="${1-}"
      [ -n "$pr" ] || { usage; exit 2; }
      prepare_run "$pr"
      ;;
    ci-dispatch)
      local pr="${1-}"
      [ -n "$pr" ] || { usage; exit 2; }
      shift
      ci_dispatch "$pr" "$@"
      ;;
    merge-verify)
      local pr="${1-}"
      [ -n "$pr" ] || { usage; exit 2; }
      merge_verify "$pr"
      ;;
    merge-run | merge-recover)
      merge_run "$merge_pr" "$auto_merge" "$recovery_oid" "$replacement_head" "$body_path"
      ;;
    merge-complete)
      merge_complete "$1" "$2"
      ;;
    *)
      usage
      exit 2
      ;;
  esac
}

main "$@"
