#!/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

# Install creator cleanup before even the Git preflight or argument validation.
# 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")"
if [ -n "${OPENCLAW_PR_ANCHOR_REPO_ROOT:-}" ] &&
  [ "${OPENCLAW_PR_ANCHOR_CREATOR_PID:-}" = "$$" ] && [ "${OPENCLAW_PR_ANCHOR_FD:-}" = 9 ]; then
  trap 'node "$script_parent_dir/pr-lib/materialized-anchor.mjs" retire "$script_self" || true' EXIT
  trap 'exit 129' HUP
  trap 'exit 130' INT
  trap 'exit 131' QUIT
  trap 'exit 143' TERM
fi

# This bootstrap stays inline: no local helper is trusted before code selection.
pr_git() { "${OPENCLAW_PR_GIT:-${GIT_EXEC:-git}}" "$@"; }
pr_run_bounded() (
  set +e
  # Separate jobs let the deadline reap a hung launcher and its children.
  set -m
  "$@" &
  local tool_pid=$! timer_pid result
  (/bin/sleep 10; kill -KILL -- "-$tool_pid" 2>/dev/null) &
  timer_pid=$!
  wait "$tool_pid" 2>/dev/null
  result=$?
  kill -KILL -- "-$timer_pid" 2>/dev/null
  wait "$timer_pid" 2>/dev/null
  exit "$result"
)

GIT_EXEC=$(command -v "${OPENCLAW_PR_GIT:-${GIT_EXEC:-git}}" 2>/dev/null || true)
if [ -n "$GIT_EXEC" ]; then
  case "$GIT_EXEC" in /*) ;; *) GIT_EXEC="$PWD/$GIT_EXEC" ;; esac
fi
if [ -z "$GIT_EXEC" ] || ! pr_run_bounded "$GIT_EXEC" --version >/dev/null 2>&1; then
  echo "scripts/pr git preflight failed or exceeded 10 seconds: ${GIT_EXEC:-${OPENCLAW_PR_GIT:-git (not found on PATH)}}" >&2
  echo "xcode-select -p: $(pr_run_bounded xcode-select -p 2>/dev/null || printf 'unavailable')" >&2
  echo "export DEVELOPER_DIR=/Applications/Xcode.app/Contents/Developer or set OPENCLAW_PR_GIT" >&2
  exit 1
fi
export GIT_EXEC
export OPENCLAW_PR_GIT="$GIT_EXEC"

# Older trusted wrappers use bare Git before reaching a newer supervisor.
# Keep this Git-only adapter across exec; OS temp reaping owns its lifetime.
# Matching inodes let reentry reuse it.
pr_path_git=$(type -P git 2>/dev/null || true)
case "$pr_path_git" in /*|"") ;; *) pr_path_git="$PWD/$pr_path_git" ;; esac
if [ -z "$pr_path_git" ] || [ ! "$pr_path_git" -ef "$GIT_EXEC" ]; then
  pr_git_path_dir=$(mktemp -d "${TMPDIR:-/tmp}/openclaw-pr-git.XXXXXX")
  if ! ln -s "$GIT_EXEC" "$pr_git_path_dir/git"; then
    rm -rf "$pr_git_path_dir"
    exit 1
  fi
  pr_git_path_dir=$(cd "$pr_git_path_dir" && pwd -P)
  export PATH="$pr_git_path_dir:$PATH"
fi
unset pr_path_git pr_git_path_dir

# 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 | workspace | ci-dispatch)
      printf 'advisory\n'
      ;;
    gc | lock-recover | tooling-recover | review-init | review-checkout-main | review-checkout-pr | review-claim | review-guard | review-artifacts-init | review-validate-artifacts | review-tests | prepare-init | prepare-correction-init | prepare-correction-continue | prepare-correction-review-init | prepare-baseline-refresh | prepare-baseline-successor | prepare-baseline-signing-request | prepare-baseline-signing-continue | 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

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" ]
}

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-}"

# Only this explicit recovery may select current main over a retained workflow.
# Parse before source selection, then parse normally again before taking a lock.
PR_WORKFLOW_RECOVERY_OUTCOME=""
PR_WORKFLOW_RECOVERY_HEAD=""
pr_read_current_workflow_request() {
  local argument found=0
  for argument in "$@"; do
    [ "$argument" != --current-workflow ] || found=$((found + 1))
  done
  [ "$found" -ne 0 ] || return 0
  [ "$found" -eq 1 ] && [ "${1-}" = merge-recover ] &&
    [[ "${2-}" =~ ^[1-9][0-9]*$ ]] && [[ "${3-}" =~ ^[0-9a-f]{40}$ ]] &&
    [ "${4-}" = --confirmed-operator-recovery ] || return 2
  PR_WORKFLOW_RECOVERY_OUTCOME="$3"
  shift 4
  local admin=false confirmed=false body=false current=false
  while [ "$#" -gt 0 ]; do
    case "$1" in
      --current-workflow) [ "$current" = false ] || return 2; current=true; shift ;;
      --replacement-head)
        [ -z "$PR_WORKFLOW_RECOVERY_HEAD" ] && [[ "${2-}" =~ ^[0-9a-f]{40}$ ]] || return 2
        PR_WORKFLOW_RECOVERY_HEAD="$2"; shift 2 ;;
      --body-file) [ "$body" = false ] && [ -n "${2-}" ] || return 2; body=true; shift 2 ;;
      --admin-evidence) [ "$admin" = false ] && [ -n "${2-}" ] || return 2; admin=true; shift 2 ;;
      --confirmed-operator-admin) [ "$confirmed" = false ] || return 2; confirmed=true; shift ;;
      *) return 2 ;;
    esac
  done
  [ "$current" = true ] && [ "$admin" = "$confirmed" ] &&
    { [ -n "$PR_WORKFLOW_RECOVERY_HEAD" ] || [ "$admin" = true ]; }
}
pr_read_current_workflow_request "$@" || {
  echo "--current-workflow requires confirmed merge-recover with an exact outcome and either a replacement head or confirmed same-head admin recovery." >&2
  exit 2
}

# This bootstrap cannot load helpers from an unverified extracted tree.
pr_batch_wrapper_git() {
  node --input-type=module - "$GIT_EXEC" "$@" <<'EOF_WRAPPER_GIT'
import { spawnSync } from "node:child_process";
import { fstatSync, lstatSync, readSync, realpathSync } from "node:fs";
import { join, resolve } from "node:path";
import { fileURLToPath } from "node:url";
const [binary, operation, repository, revision, ...paths] = process.argv.slice(2);
function git(directory, args, input) {
  const result = spawnSync(binary, ["-C", directory, ...args], {
    input, maxBuffer: Infinity, stdio: ["pipe", "pipe", "ignore"],
    env: { ...process.env, GIT_NO_LAZY_FETCH: "1" },
  });
  if (result.error || result.signal || result.status !== 0) throw new Error("Git read failed");
  return result.stdout;
}
const oid = /^(?:[a-f0-9]{40}|[a-f0-9]{64})$/;
try {
  if (operation.startsWith("workflow-")) {
    if (!/^[1-9][0-9]*$/.test(revision)) throw new Error("Invalid workflow PR");
    const ref = `refs/openclaw/pr-workflow-bindings/${revision}`;
    const run = args => git(repository, args).toString("utf8").trim();
    const owner = realpathSync(run(["rev-parse", "--path-format=absolute", "--git-common-dir"]));
    const remote = run(["remote", "get-url", "origin"]);
    const url = /^[a-z][a-z0-9+.-]*:\/\//i.test(remote) ? new URL(remote) : null;
    const scp = !url && /^(?:([^/@:]+)@)?(\[[^\]]+\]|[^/:]+):(.+)$/.exec(remote);
    // SCP paths can be relative to an SSH user's home. Preserve that transport,
    // user, path case and URL ports instead of conflating different endpoints.
    const identity = url ? url.protocol === "file:"
      ? realpathSync(fileURLToPath(url)) : url.href
      : scp ? JSON.stringify(["scp", scp[1] ?? "", scp[2].toLowerCase(), scp[3]])
      : realpathSync(resolve(repository, remote));
    if (!identity || url && (url.username && url.protocol !== "ssh:" || url.password)) throw new Error("Invalid repository identity");
    const read = () => {
      const symbolic = spawnSync(binary, ["-C", repository, "symbolic-ref", "--quiet", ref], { stdio: "ignore" });
      if (symbolic.error || symbolic.signal || symbolic.status !== 1) throw new Error("Invalid symbolic workflow binding");
      const listing = run(["for-each-ref", "--format=%(objectname) %(symref)", ref]);
      if (!listing) return null;
      if (!oid.test(listing)) throw new Error("Invalid workflow binding ref");
      const headers = run(["cat-file", "commit", listing]).split("\n\n", 1)[0].split("\n");
      const parents = headers.filter(line => line.startsWith("parent ")).map(line => line.slice(7));
      const entries = run(["ls-tree", listing]).split("\n");
      if (parents.length !== 1 || entries.length !== 1 || !/^100644 blob [a-f0-9]+\tbinding.json$/.test(entries[0])) {
        throw new Error("Invalid workflow binding tree");
      }
      if (Number(run(["cat-file", "-s", `${listing}:binding.json`])) > 4096) throw new Error("Oversized workflow binding");
      const record = JSON.parse(run(["show", `${listing}:binding.json`]));
      if (JSON.stringify(Object.keys(record).sort()) !== JSON.stringify(["owner", "pr", "repository", "source", "version"]) ||
          record.version !== 1 || record.pr !== revision || record.owner !== owner || record.repository !== identity ||
          !oid.test(record.source) || parents[0] !== record.source ||
          run(["cat-file", "-t", record.source]) !== "commit") throw new Error("Workflow binding identity mismatch");
      return { oid: listing, source: record.source };
    };
    const requireSource = (source, anchor = "refs/remotes/origin/main") => {
      if (!oid.test(source)) throw new Error("Invalid workflow source");
      run(["merge-base", "--is-ancestor", source, anchor]);
    };
    const current = read();
    const requireRecovery = (source, outcome, replacementHead, anchor = "refs/remotes/origin/main") => {
      const symbolicAnchor = spawnSync(binary, ["-C", repository, "symbolic-ref", "--quiet", anchor], { stdio: "ignore" });
      if (symbolicAnchor.error || symbolicAnchor.signal || symbolicAnchor.status !== 1) {
        throw new Error("Refusing symbolic or unreadable current workflow anchor");
      }
      if (!current || !oid.test(outcome) || (replacementHead !== "" && !oid.test(replacementHead)) ||
          source !== run(["rev-parse", "--verify", `${anchor}^{commit}`]) ||
          !["https://github.com/openclaw/openclaw", "https://github.com/openclaw/openclaw.git",
            "git@github.com:openclaw/openclaw.git", "ssh://git@github.com/openclaw/openclaw.git"].includes(run(["config", "--get", "remote.origin.url"]))) {
        throw new Error("Current workflow recovery requires official current main and a retained workflow");
      }
      requireSource(current.source, anchor);
      const outcomeRef = `refs/openclaw/pr-merge-outcomes/${revision}`;
      const symbolic = spawnSync(binary, ["-C", repository, "symbolic-ref", "--quiet", outcomeRef], { stdio: "ignore" });
      if (symbolic.error || symbolic.signal || symbolic.status !== 1 ||
          run(["rev-parse", "--verify", outcomeRef]) !== outcome || run(["cat-file", "-t", outcome]) !== "commit") {
        throw new Error("Current workflow recovery outcome changed");
      }
      const record = JSON.parse(run(["show", `${outcome}:outcome.json`]));
      const cancelledAuto = replacementHead !== "" && record.route === "auto" &&
        record.cancellation?.state === "confirmed";
      // This admits current trusted code, not another dispatch. The existing
      // provider-rejection owner must still qualify exact HTTP 405 capture bytes
      // and revalidate the unchanged head and live admin authority before writing.
      const rejectedAdmin = replacementHead === "" && record.route === "admin" &&
        record.accepted === false &&
        record.priorCiAdmin?.dispatchTransport === "rest";
      if (record.version !== 1 || record.pr !== Number(revision) ||
          record.base !== "main" || record.phase !== "intent" ||
          record.method !== "squash" || record.landed !== null || !(cancelledAuto || rejectedAdmin)) {
        throw new Error("Current workflow recovery requires a confirmed cancelled nonqueue auto outcome or an unaccepted same-head REST admin intent");
      }
    };
    if (operation === "workflow-recovery-select" || operation === "workflow-recovery-provenance") {
      const [outcome, replacementHead, anchor = "refs/remotes/origin/main"] = paths;
      const source = operation === "workflow-recovery-select" ? run(["rev-parse", "--verify", `${anchor}^{commit}`]) : paths[3];
      requireRecovery(source, outcome, replacementHead, anchor);
      process.stdout.write(operation === "workflow-recovery-select" ? `${current.oid}\t${source}\n` :
        JSON.stringify({ binding: current.oid, from: current.source, to: source }));
    } else if (operation === "workflow-read" || operation === "workflow-inspect") {
      if (current && operation === "workflow-read") requireSource(current.source);
      if (current) process.stdout.write(`${current.oid}\t${current.source}\n`);
    } else if (operation === "workflow-context-write") {
      const [source, binding, creator, bootstrapFlag, outcome = "", replacementHead = ""] = paths;
      const bootstrap = bootstrapFlag === "1";
      if (outcome) requireRecovery(source, outcome, replacementHead);
      if (!oid.test(source) || (current?.oid ?? "") !== binding || !outcome && !bootstrap && current && current.source !== source ||
          !/^[1-9][0-9]*$/.test(creator)) throw new Error("Workflow selection changed before handoff");
      if (bootstrap) {
        if (source !== run(["rev-parse", "--verify", "HEAD^{commit}"])) throw new Error("Canonical bootstrap changed");
      } else requireSource(source);
      process.stdout.write(JSON.stringify({ version: 1, owner, repository: identity, pr: revision, source, binding, creator, bootstrap,
        ...(outcome ? { recovery: { outcome, replacementHead } } : {}) }));
    } else if (operation === "workflow-context-read") {
      const held = fstatSync(8);
      if (!held.isFile() || held.uid !== process.getuid() || (held.mode & 0o777) !== 0o400 || held.nlink !== 0 ||
          held.size < 1 || held.size > 4096 || `${held.dev}:${held.ino}` !== process.env.OPENCLAW_PR_WORKFLOW_CONTEXT) {
        throw new Error("Invalid workflow selection descriptor");
      }
      const bytes = Buffer.alloc(held.size);
      if (readSync(8, bytes, 0, bytes.length, 0) !== bytes.length) throw new Error("Incomplete workflow selection");
      const selection = JSON.parse(bytes.toString("utf8"));
      const [creator, supervisor, outcome = "", replacementHead = ""] = paths;
      if (outcome) {
        if (JSON.stringify(selection.recovery) !== JSON.stringify({ outcome, replacementHead }) || selection.bootstrap) {
          throw new Error("Workflow recovery selection changed across handoff");
        }
        requireRecovery(selection.source, outcome, replacementHead);
      }
      const supervised = selection.creator === supervisor && process.env.OPENCLAW_PR_DEDICATED_PROCESS_GROUP === "1" &&
        process.env.OPENCLAW_PR_LOCK_NOTIFY_FD === "3" && process.env.OPENCLAW_PR_LOCK_SUPERVISOR_PID === supervisor;
      const keys = ["binding", "bootstrap", "creator", "owner", "pr", "repository", "source", "version", ...(outcome ? ["recovery"] : [])].sort();
      if (JSON.stringify(Object.keys(selection).sort()) !== JSON.stringify(keys) ||
          selection.version !== 1 || selection.owner !== owner || selection.repository !== identity || selection.pr !== revision ||
          typeof selection.bootstrap !== "boolean" ||
          !oid.test(selection.source) || (selection.creator !== creator && !supervised) ||
          selection.binding !== (current?.oid ?? "") || !outcome && !selection.bootstrap && current && current.source !== selection.source) {
        throw new Error("Workflow selection changed across handoff");
      }
      if (!selection.bootstrap) requireSource(selection.source);
      process.stdout.write(`${selection.binding || "-"}\t${selection.source}\t${selection.bootstrap ? "1" : "0"}\n`);
    } else if (operation === "workflow-cold-select") {
      const [bootstrap, expected, main] = paths;
      if (!oid.test(bootstrap) || !oid.test(main) || (current?.oid ?? "") !== expected) {
        throw new Error("Workflow binding changed during cold acquisition");
      }
      let selected;
      if (current) {
        requireSource(current.source, main);
        selected = current.source;
      } else {
        const ancestry = spawnSync(binary, ["-C", repository, "merge-base", "--is-ancestor", bootstrap, main],
          { stdio: "ignore", env: { ...process.env, GIT_NO_LAZY_FETCH: "1" } });
        if (ancestry.error || ancestry.signal || ![0, 1].includes(ancestry.status)) throw new Error("Cannot qualify canonical bootstrap");
        selected = ancestry.status === 0 ? bootstrap : main;
      }
      const closure = source => {
        const inventory = run(["show", `${source}:scripts/pr-lib/wrapper-components.txt`]).split("\n");
        return git(repository, ["ls-tree", "-r", "-z", "--full-tree", source, "--", "scripts/pr", "scripts/pr-lib", ...inventory]);
      };
      // Loaded helpers belong to the captured bootstrap, even if files move
      // during acquisition. Compare immutable closures before admitting it.
      if (!closure(bootstrap).equals(closure(selected))) {
        throw new Error(`Cold workflow source differs from the running canonical wrapper ${bootstrap}; selected source ${selected}`);
      }
      process.stdout.write(selected);
    } else if (operation === "workflow-admit") {
      const [source, expected, anchor = "refs/remotes/origin/main", outcome = "", replacementHead = ""] = paths;
      if (outcome) requireRecovery(source, outcome, replacementHead, anchor);
      if (!oid.test(source) || (current?.oid ?? "") !== expected || !outcome && current && current.source !== source) {
        throw new Error("Workflow binding changed before admission");
      }
      requireSource(source, anchor);
      if (current) process.stdout.write(current.oid);
      else {
        const record = JSON.stringify({ version: 1, repository: identity, owner, pr: revision, source });
        const blob = git(repository, ["hash-object", "-w", "--stdin"], record).toString().trim();
        const tree = git(repository, ["mktree"], `100644 blob ${blob}\tbinding.json\n`).toString().trim();
        const commit = git(repository, ["-c", "commit.gpgsign=false", "commit-tree", tree, "-p", source],
          "Native PR workflow binding\n").toString().trim();
        run(["update-ref", "--no-deref", ref, commit, "0".repeat(source.length)]);
        process.stdout.write(commit);
      }
    } else if (operation === "workflow-retire") {
      if (!current || current.oid !== paths[0]) throw new Error("Workflow binding changed before completion");
      requireSource(current.source, paths[1]);
      run(["update-ref", "--no-deref", "-d", ref, current.oid]);
    } else throw new Error("Unknown workflow operation");
  } else if (operation === "differences") {
    const queries = paths.flatMap(path => [`HEAD:${path}`, `${revision}:${path}`]);
    const rows = git(repository, ["cat-file", "--batch-check=%(objectname)"], queries.join("\n") + "\n")
      .toString("utf8").split("\n");
    if (rows.pop() !== "" || rows.length !== queries.length) throw new Error("Incomplete revisions");
    process.stdout.write(paths.filter((_, index) => {
      const [head, anchor] = rows.slice(index * 2, index * 2 + 2);
      return !oid.test(head) || !oid.test(anchor) || head !== anchor;
    }).join("\n"));
  } else if (operation === "verify") {
    const [directory, ...components] = paths;
    const listing = git(repository, ["ls-tree", "-r", "-z", "--full-tree", revision, "--", ...components]);
    const text = listing.toString("utf8");
    const rows = text.split("\0");
    if (!Buffer.from(text).equals(listing) || rows.pop() !== "" || rows.length === 0) {
      throw new Error("Incomplete anchor listing");
    }
    const names = new Set();
    const directories = new Set();
    const expected = [];
    if (!lstatSync(directory).isDirectory()) throw new Error("Invalid extraction root");
    for (const row of rows) {
      const entry = /^(100644|100755) blob ([a-f0-9]+)\t([^\0]+)$/.exec(row);
      if (!entry || !oid.test(entry[2])) throw new Error("Invalid anchor entry");
      const [, , hash, path] = entry;
      if (names.has(path) || path.split("/").some(part => !part || part === "." || part === "..")) {
        throw new Error("Invalid anchor path");
      }
      let parent = "";
      for (const part of path.split("/").slice(0, -1)) {
        parent = parent ? `${parent}/${part}` : part;
        if (!directories.has(parent)) {
          if (!lstatSync(join(directory, parent)).isDirectory()) throw new Error("Non-directory anchor parent");
          directories.add(parent);
        }
      }
      if (!lstatSync(join(directory, path)).isFile()) throw new Error("Non-regular anchor file");
      names.add(path);
      expected.push(hash);
    }
    if (components.some(path => !names.has(path) && !directories.has(path))) throw new Error("Incomplete anchor components");
    // Git's stdin path quoting uses C/octal escapes, not JSON's Unicode escapes.
    const input = [...names].map(path => '"' + join(directory, path).replace(/[\x00-\x1f\x7f\\"]/g, char =>
      char === "\\" || char === '"' ? "\\" + char : "\\" + char.charCodeAt(0).toString(8).padStart(3, "0"),
    ) + '"').join("\n") + "\n";
    const actual = git(repository, ["hash-object", "--no-filters", "--stdin-paths"], input);
    if (!actual.equals(Buffer.from(expected.join("\n") + "\n"))) throw new Error("Anchor bytes differ");
  } else {
    throw new Error("Unknown wrapper Git operation");
  }
} catch (error) {
  if (operation.startsWith("workflow-")) console.error(error.message);
  process.exitCode = 1;
}
EOF_WRAPPER_GIT
}

PR_WORKFLOW_PR=""
PR_WORKFLOW_BINDING_OID=""
PR_WORKFLOW_SOURCE_REVISION=""
PR_WORKFLOW_BOOTSTRAP=0
PR_WORKFLOW_RECOVERY_PROVENANCE=""
PR_WORKFLOW_TRUST_ANCHOR="refs/remotes/origin/main"
pr_read_workflow_binding() {
  local classification binding tracking_status=0
  classification=$(pr_subcommand_classification "$requested_subcommand" 2>/dev/null || true)
  [ "$classification" = landing ] || return 0
  case "$requested_subcommand" in gc | lock-recover) return 0 ;; esac
  [[ "${2-}" =~ ^[1-9][0-9]*$ ]] || return 0
  PR_WORKFLOW_PR="$2"
  if [ -n "${OPENCLAW_PR_WORKFLOW_CONTEXT:-}" ]; then
    binding=$(pr_batch_wrapper_git workflow-context-read "$canonical_repo_root" "$PR_WORKFLOW_PR" "$$" "$PPID" \
      "$PR_WORKFLOW_RECOVERY_OUTCOME" "$PR_WORKFLOW_RECOVERY_HEAD") || return 1
    IFS=$'\t' read -r PR_WORKFLOW_BINDING_OID PR_WORKFLOW_SOURCE_REVISION PR_WORKFLOW_BOOTSTRAP <<< "$binding"
  else
    if [ -n "$PR_WORKFLOW_RECOVERY_OUTCOME" ]; then
      binding=$(pr_batch_wrapper_git workflow-recovery-select "$canonical_repo_root" "$PR_WORKFLOW_PR" \
        "$PR_WORKFLOW_RECOVERY_OUTCOME" "$PR_WORKFLOW_RECOVERY_HEAD") || return 1
      IFS=$'\t' read -r PR_WORKFLOW_BINDING_OID PR_WORKFLOW_SOURCE_REVISION <<< "$binding"
      return 0
    fi
    if pr_git -C "$canonical_repo_root" symbolic-ref -q refs/remotes/origin/main >/dev/null 2>&1; then
      echo "Refusing a symbolic main tracking ref for workflow selection." >&2
      return 1
    fi
    pr_git -C "$canonical_repo_root" show-ref --verify --quiet refs/remotes/origin/main || tracking_status=$?
    if [ "$tracking_status" -eq 1 ] && [ "$script_self" = "$canonical_repo_root/scripts/pr" ]; then
      # Only canonical code may bootstrap. A retained record is data until the
      # locked acquisition authenticates its source against canonical main.
      binding=$(pr_batch_wrapper_git workflow-inspect "$canonical_repo_root" "$PR_WORKFLOW_PR") || return 1
      IFS=$'\t' read -r PR_WORKFLOW_BINDING_OID PR_WORKFLOW_SOURCE_REVISION <<< "$binding"
      PR_WORKFLOW_SOURCE_REVISION=$(pr_git -C "$canonical_repo_root" rev-parse --verify 'HEAD^{commit}') || return 1
      PR_WORKFLOW_BOOTSTRAP=1
    else
      [ "$tracking_status" -le 1 ] || return 1
      binding=$(pr_batch_wrapper_git workflow-read "$canonical_repo_root" "$PR_WORKFLOW_PR") || return 1
      IFS=$'\t' read -r PR_WORKFLOW_BINDING_OID PR_WORKFLOW_SOURCE_REVISION <<< "$binding"
    fi
  fi
  [ "$PR_WORKFLOW_BINDING_OID" != - ] || PR_WORKFLOW_BINDING_OID=""
  if [ "$PR_WORKFLOW_BOOTSTRAP" = 1 ]; then
    [ "$script_self" = "$canonical_repo_root/scripts/pr" ] || return 1
  fi
  if [ -n "${OPENCLAW_PR_WORKFLOW_CONTEXT:-}" ] || [ "$PR_WORKFLOW_BOOTSTRAP" = 1 ]; then
    verify_pr_workflow_source "$PR_WORKFLOW_SOURCE_REVISION" "$script_parent_dir/.." || return 1
  fi
}

verify_pr_workflow_source() {
  local inventory component
  local components=(scripts/pr scripts/pr-lib)
  inventory=$(pr_git -C "$canonical_repo_root" show "$1:scripts/pr-lib/wrapper-components.txt") || return 1
  while IFS= read -r component; do components+=("$component"); done <<< "$inventory"
  pr_batch_wrapper_git verify "$canonical_repo_root" "$1" "$2" "${components[@]}"
}

hold_pr_workflow_selection() {
  [ -n "$PR_WORKFLOW_PR" ] && [ -z "${OPENCLAW_PR_WORKFLOW_CONTEXT:-}" ] || return 0
  verify_pr_workflow_source "$1" "$2" || return 1
  local context
  context=$(mktemp "${TMPDIR:-/tmp}/openclaw-pr-selection.XXXXXX") || return 1
  if ! pr_batch_wrapper_git workflow-context-write "$canonical_repo_root" "$PR_WORKFLOW_PR" \
    "$1" "$PR_WORKFLOW_BINDING_OID" "$$" "$PR_WORKFLOW_BOOTSTRAP" \
    "$PR_WORKFLOW_RECOVERY_OUTCOME" "$PR_WORKFLOW_RECOVERY_HEAD" > "$context" || ! chmod 400 "$context"; then
    rm -f "$context"
    return 1
  fi
  # FD 8 carries selection; FD 9 remains the archive cleanup owner.
  # Positional reads keep exec/child reentry independent of shared FD offsets.
  exec 8< "$context"
  rm "$context"
  OPENCLAW_PR_WORKFLOW_CONTEXT=$(node -e 'const s=require("node:fs").fstatSync(8); process.stdout.write(`${s.dev}:${s.ino}`)') || return 1
  export OPENCLAW_PR_WORKFLOW_CONTEXT
}

admit_pr_workflow_binding() {
  [ -n "$PR_WORKFLOW_PR" ] || return 0
  [ "$PR_WORKFLOW_PR" = "$1" ] && [ -n "$PR_WORKFLOW_SOURCE_REVISION" ] || return 1
  local qualification_ref="" main=""
  if [ "$PR_WORKFLOW_BOOTSTRAP" = 1 ] || [ -n "$PR_WORKFLOW_RECOVERY_OUTCOME" ]; then
    qualification_ref="refs/openclaw/pr-workflow-qualification/$1/$PR_OPERATION_LOCK_OWNER_OID"
    local ref_status=0 symbolic_status=0 selected
    pr_git -C "$canonical_repo_root" symbolic-ref -q "$qualification_ref" >/dev/null 2>&1 || symbolic_status=$?
    [ "$symbolic_status" -eq 1 ] || { echo "Refusing symbolic or unreadable qualification ref: $qualification_ref." >&2; return 1; }
    pr_git -C "$canonical_repo_root" show-ref --verify --quiet "$qualification_ref" || ref_status=$?
    [ "$ref_status" -eq 1 ] || { echo "Workflow qualification ref already exists; preserve $qualification_ref." >&2; return 1; }
    mark_pr_operation_side_effects_started || return 1
    if ! fetch_canonical_main "$qualification_ref"; then
      echo "Cold workflow acquisition failed; inspect $qualification_ref before any cleanup." >&2
      return 1
    fi
    main=$(pr_git -C "$canonical_repo_root" rev-parse --verify "$qualification_ref^{commit}") || return 1
    PR_WORKFLOW_TRUST_ANCHOR="$main"
    if [ -n "$PR_WORKFLOW_RECOVERY_OUTCOME" ]; then
      # Current main is selected only for this operation. Keep the historical
      # binding intact so normal reconciliation still uses its admitted source.
      verify_pr_workflow_source "$main" "$script_parent_dir/.." || {
        echo "Current main workflow changed; rerun recovery from current main. Preserving $qualification_ref." >&2
        return 1
      }
      selected="$main"
    elif ! selected=$(pr_batch_wrapper_git workflow-cold-select "$canonical_repo_root" "$1" \
      "$PR_WORKFLOW_SOURCE_REVISION" "$PR_WORKFLOW_BINDING_OID" "$main"); then
      echo "Cold workflow qualification failed; use the admitted source from a qualified checkout. Preserving $qualification_ref." >&2
      return 1
    fi
    verify_pr_workflow_source "$PR_WORKFLOW_SOURCE_REVISION" "$script_parent_dir/.." || return 1
    PR_WORKFLOW_SOURCE_REVISION="$selected"
  fi
  # Verify again under the operation lock: initial selection may have waited
  # behind another command, and only admitted bytes may mint a lasting pin.
  verify_pr_workflow_source "$PR_WORKFLOW_SOURCE_REVISION" "$script_parent_dir/.." || return 1
  pr_operation_lock_owner_is_current "$canonical_repo_root" "$PR_OPERATION_LOCK_REF" "$PR_OPERATION_LOCK_OWNER_OID" || return 1
  mark_pr_operation_side_effects_started || return 1
  PR_WORKFLOW_BINDING_OID=$(pr_batch_wrapper_git workflow-admit "$canonical_repo_root" "$1" \
    "$PR_WORKFLOW_SOURCE_REVISION" "$PR_WORKFLOW_BINDING_OID" "$PR_WORKFLOW_TRUST_ANCHOR" \
    "$PR_WORKFLOW_RECOVERY_OUTCOME" "$PR_WORKFLOW_RECOVERY_HEAD") || return 1
  if [ -n "$PR_WORKFLOW_RECOVERY_OUTCOME" ]; then
    PR_WORKFLOW_RECOVERY_PROVENANCE=$(pr_batch_wrapper_git workflow-recovery-provenance "$canonical_repo_root" "$1" \
      "$PR_WORKFLOW_RECOVERY_OUTCOME" "$PR_WORKFLOW_RECOVERY_HEAD" "$PR_WORKFLOW_TRUST_ANCHOR" "$PR_WORKFLOW_SOURCE_REVISION") || return 1
  fi
  if [ -n "$qualification_ref" ]; then
    # Preserve acquisition evidence until admission succeeds; retire only our
    # captured value, never a changed qualification ref.
    pr_operation_lock_owner_is_current "$canonical_repo_root" "$PR_OPERATION_LOCK_REF" "$PR_OPERATION_LOCK_OWNER_OID" || return 1
    pr_git -C "$canonical_repo_root" update-ref --no-deref -d "$qualification_ref" "$main" || return 1
  fi
  exec 8<&-
  unset OPENCLAW_PR_WORKFLOW_CONTEXT
}

# Keep dependency sourcing independent of wrapper selection. In particular, an
# older anchor still supplies its own package/version policy to this bootstrap.
pr_materialize_dependencies() {
  # Location discovery uses only Git and Node built-ins, before dependency repair.
  [ "$requested_subcommand" != workspace ] || return 0
  node --input-type=module - "$1" "$canonical_repo_root" "$2" "${3:-${PR_WORKFLOW_SOURCE_REVISION:-}}" <<'EOF_TOOLING'
import { execFileSync, spawnSync } from "node:child_process";
import { createHash } from "node:crypto";
import {
  existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, readdirSync,
  readlinkSync, realpathSync, renameSync, rmSync, symlinkSync, utimesSync,
} from "node:fs";
import { homedir } from "node:os";
import { delimiter, dirname, join, resolve } from "node:path";

function repository(url, root) {
  if (/^(?:\.?\.?\/|\/)/.test(url)) return realpathSync(resolve(root, url));
  const parsed = new URL(url.replace(/^git@([^:]+):/, "ssh://git@$1/"));
  if (parsed.protocol === "file:") return realpathSync(decodeURIComponent(parsed.pathname));
  return `${parsed.hostname.toLowerCase()}/${parsed.pathname.replace(/^\/|\/$/g, "").replace(/\.git$/, "").toLowerCase()}`;
}

function sweep(cache, current) {
  const entries = readdirSync(cache)
    .filter((name) => /^(?:[a-f0-9]{64}|install-[0-9]+-[A-Za-z0-9]+)$/.test(name))
    .map((name) => {
      const path = join(cache, name);
      return { name, path, stat: lstatSync(path, { throwIfNoEntry: false }) };
    })
    .filter(({ stat }) => stat)
    .sort((a, b) => b.stat.mtimeMs - a.stat.mtimeMs);
  let retained = 0;
  for (const { name, path, stat } of entries) {
    if (path === current || !stat?.isDirectory() || stat.uid !== process.getuid?.() ||
        (stat.mode & 0o777) !== 0o700) continue;
    // Creator PIDs survive the wrapper/supervisor exec chain. PID reuse only
    // delays collection; a live consumer must never lose its pinned packages.
    const users = join(path, ".pr-users");
    const pids = existsSync(users) ? readdirSync(users) : [];
    if (name.startsWith("install-")) pids.push(name.split("-")[1]);
    if (pids.some((pid) => {
      if (!/^[1-9][0-9]*$/.test(pid)) return true;
      try { process.kill(Number(pid), 0); return true; }
      catch (error) { return error.code !== "ESRCH"; }
    })) continue;
    if (!name.startsWith("install-") && existsSync(join(path, ".pr-complete"))) {
      // Adjacent main heads get a small reuse window; live and current installs
      // do not consume it. Age alone cannot bound one new tree per landing.
      if (retained++ < 4) continue;
    } else if (stat.mtimeMs >= Date.now() - 86_400_000) {
      continue;
    }
    rmSync(path, { recursive: true, force: true });
  }
}

function materializeDependencies(owner, canonicalPath, destination, revision) {
  const canonical = realpathSync(canonicalPath);
  const gitBinary = process.env.OPENCLAW_PR_GIT;
  const git = (root, args) => execFileSync(gitBinary, ["-C", root, ...args], {
    encoding: "utf8", timeout: 300_000, maxBuffer: 32 * 1024 * 1024,
    stdio: ["ignore", "pipe", "pipe"],
  }).trimEnd();
  const config = (root, key, type = "--path") => {
    const result = spawnSync(gitBinary, ["-C", root, "config", type, "--get", key], {
      encoding: "utf8", timeout: 300_000,
    });
    if (result.status === 1) return "";
    if (result.status !== 0) throw new Error(`Cannot read Git setting ${key} in ${root}.`);
    return result.stdout.trim();
  };
  const requested = process.env.OPENCLAW_PR_TOOLING_ROOT || config(canonical, "openclaw.pr.toolingRoot");
  const root = realpathSync(resolve(canonical, requested || "."));
  if (requested) {
    if (git(root, ["rev-parse", "--is-inside-work-tree"]) !== "true" ||
        realpathSync(git(root, ["rev-parse", "--show-toplevel"])) !== root ||
        config(root, "core.sparseCheckout", "--bool") === "true" ||
        repository(git(root, ["remote", "get-url", "origin"]), root) !==
          repository(git(canonical, ["remote", "get-url", "origin"]), canonical)) {
      throw new Error(`Tooling root must be a full Git work tree of this repository: ${root}`);
    }
    console.error(`scripts/pr tooling root: ${root}`);
  }
  const materialize = (installed, target) => {
    const result = spawnSync(process.execPath, [owner, join(installed, "node_modules"), target], {
      encoding: "utf8", stdio: ["ignore", "inherit", "pipe"],
    });
    if (result.stderr) process.stderr.write(result.stderr);
    if (result.error) throw result.error;
    // Historical anchors returned 1 instead of 78 for the same refusal.
    const drift = result.status === 78 || (result.status === 1 &&
      /^Installed scripts\/pr dependency '[^\r\n]+' has version [^\r\n]+; the trust anchor requires [^\r\n]+\.$/m.test(result.stderr));
    if (result.status !== 0 && !drift) throw new Error(`Dependency materialization failed in ${target}.`);
    return result.status === 0;
  };
  if (materialize(root, destination)) return;

  // The wrapper has already authenticated this source and verified its extracted
  // inventory. Recheck ancestry and manifest identity before executing install hooks.
  if (!/^[a-f0-9]{40}$/.test(revision ?? "")) throw new Error("Missing verified dependency anchor.");
  git(canonical, ["merge-base", "--is-ancestor", revision, "refs/remotes/origin/main"]);
  for (const name of ["package.json", "packages/markdown-core/package.json"]) {
    const path = join(dirname(destination), name);
    if (name !== "package.json" && !existsSync(path)) continue;
    if (readFileSync(path, "utf8").trimEnd() !== git(canonical, ["show", `${revision}:${name}`])) {
      throw new Error(`Dependency manifest differs from verified anchor: ${name}`);
    }
  }
  const tree = git(canonical, ["rev-parse", `${revision}^{tree}`]);
  // Full source includes every workspace, patch, and lifecycle input. Keying by
  // its tree deliberately trades some reuse for an exact frozen install contract.
  const key = createHash("sha256").update(JSON.stringify([
    tree, process.platform, process.arch, process.versions.modules,
  ])).digest("hex");
  const cache = join(homedir(), ".cache", "openclaw", "pr-dependencies");
  mkdirSync(cache, { recursive: true, mode: 0o700 });
  const entry = join(cache, key);
  const complete = join(entry, ".pr-complete");
  if (!existsSync(complete)) {
    const stage = mkdtempSync(join(cache, `install-${process.ppid}-`));
    try {
      const archive = join(stage, ".pr-source.tar");
      git(canonical, ["archive", "--format=tar", `--output=${archive}`, revision]);
      execFileSync("tar", ["-xf", archive, "-C", stage]);
      rmSync(archive);
      // Verify archive bytes, including symlinks, before any package lifecycle runs.
      const sourcePaths = new Set();
      for (const row of git(canonical, ["ls-tree", "-r", "-z", revision]).split("\0").filter(Boolean)) {
        const match = /^(100644|100755|120000) blob ([a-f0-9]{40})\t([^\0]+)$/.exec(row);
        if (!match) throw new Error("Unsupported frozen install source entry.");
        const [, mode, oid, name] = match;
        sourcePaths.add(name);
        const path = join(stage, name);
        const stat = lstatSync(path);
        const data = mode === "120000"
          ? stat.isSymbolicLink() ? Buffer.from(readlinkSync(path)) : null
          : stat.isFile() && Boolean(stat.mode & 0o100) === (mode === "100755") ? readFileSync(path) : null;
        if (!data || createHash("sha1").update(`blob ${data.length}\0`).update(data).digest("hex") !== oid) {
          throw new Error(`Frozen install source differs: ${name}`);
        }
      }
      const verifyInventory = (directory, prefix = "") => {
        for (const child of readdirSync(directory)) {
          const name = prefix + child;
          const path = join(directory, child);
          if (lstatSync(path).isDirectory()) verifyInventory(path, `${name}/`);
          else if (!sourcePaths.has(name)) throw new Error(`Unexpected frozen install source: ${name}`);
        }
      };
      verifyInventory(stage);
      console.error(`Installing frozen scripts/pr dependencies for ${revision}.`);
      const bin = join(stage, ".pr-bin");
      mkdirSync(bin);
      symlinkSync(gitBinary, join(bin, "git"));
      const installEnv = { ...process.env, PATH: `${bin}${delimiter}${process.env.PATH ?? ""}` };
      // Lifecycle Git calls must not inherit a hook's checkout or index ownership.
      for (const name of ["GIT_DIR", "GIT_WORK_TREE", "GIT_COMMON_DIR", "GIT_INDEX_FILE", "GIT_PREFIX", "GIT_OBJECT_DIRECTORY", "GIT_ALTERNATE_OBJECT_DIRECTORIES"]) delete installEnv[name];
      const installed = spawnSync("pnpm", ["-C", stage, "install", "--frozen-lockfile", "--prefer-offline"], {
        stdio: "inherit", env: installEnv,
      });
      rmSync(bin, { recursive: true });
      if (installed.error) throw installed.error;
      // Validation writes pins; keep them outside the install that will move.
      const check = join(stage, ".pr-dependency-check");
      if (installed.status !== 0 || !materialize(stage, check)) {
        throw new Error("Frozen dependency installation failed.");
      }
      rmSync(check, { recursive: true });
      mkdirSync(join(stage, ".pr-complete"));
      try { renameSync(stage, entry); }
      catch (error) {
        if (!["EEXIST", "ENOTEMPTY"].includes(error.code) || !existsSync(complete)) throw error;
      }
    } finally {
      rmSync(stage, { recursive: true, force: true });
    }
  }
  mkdirSync(join(entry, ".pr-users", String(process.ppid)), { recursive: true });
  const now = new Date();
  utimesSync(entry, now, now);
  if (!materialize(entry, destination)) throw new Error("Cached dependencies do not match the trust anchor.");
  console.error(`Using frozen scripts/pr dependency installation: ${entry}`);
  try { sweep(cache, entry); }
  catch { console.error("Warning: unable to sweep stale scripts/pr dependency installations."); }
}

try {
  materializeDependencies(...process.argv.slice(2));
} catch (error) {
  console.error(`Dependency materialization refused: ${error.message}`);
  process.exitCode = 1;
}
EOF_TOOLING
}

# Select trusted wrapper code independently from the canonical repository root;
# a linked wrapper may be removed by merge-run or gc before supervision ends.
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 ! pr_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"
  pr_read_workflow_binding "$@"
  # Older parents pin only their own dependency set. The anchored child owns
  # completing its installation before any loader, supervisor, or lock starts.
  pr_materialize_dependencies "$script_parent_dir/pr-lib/materialize-dependencies.mjs" \
    "$script_parent_dir/../node_modules"
elif common_git_dir=$(pr_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]}")"
  pr_read_workflow_binding "$@"
  if [ "$PR_WORKFLOW_BOOTSTRAP" != 1 ] && [ -x "$canonical_self" ] && { [ "$script_self" != "$canonical_self" ] || [ -n "$PR_WORKFLOW_PR" ]; }; then
    linked_inventory=$(pr_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 ! pr_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=$(
      pr_git -C "$script_parent_dir" rev-parse "${pr_wrapper_components[@]/#/HEAD:}" 2>/dev/null || true
    )
    canonical_wrapper_revision=$(
      pr_git -C "$canonical_repo_root" rev-parse "${pr_wrapper_components[@]/#/HEAD:}" 2>/dev/null || true
    )
    if [ "$script_self" != "$canonical_self" ] && [ -z "$PR_WORKFLOW_PR" ] && [ -n "$linked_wrapper_revision" ] &&
      [ "$linked_wrapper_revision" = "$canonical_wrapper_revision" ] &&
      pr_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. Matching code can run in place when its command
    # does not need the materialized dependency context.
    # 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="${PR_WORKFLOW_SOURCE_REVISION:-$(pr_git -C "$script_parent_dir" rev-parse --verify refs/remotes/origin/main^{commit} 2>/dev/null || true)}"
    anchor_description="origin/main"
    anchor_reference="refs/remotes/origin/main"
    if [ -n "$PR_WORKFLOW_BINDING_OID" ]; then
      anchor_description="retained PR #$PR_WORKFLOW_PR workflow $anchor_commit"
      anchor_reference="$anchor_description"
    fi
    anchor_wrapper_revision=""
    if [ -n "$anchor_commit" ] &&
      anchor_inventory=$(pr_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=$(
        pr_git -C "$script_parent_dir" rev-parse "${anchor_components[@]/#/$anchor_commit:}" 2>/dev/null || true
      )
      canonical_wrapper_revision=$(
        pr_git -C "$canonical_repo_root" rev-parse "${anchor_components[@]/#/HEAD:}" 2>/dev/null || true
      )
    fi
    linked_matches_anchor=0
    if [ -n "$linked_wrapper_revision" ] &&
      [ "$linked_wrapper_revision" = "$anchor_wrapper_revision" ]; then
      linked_matches_anchor=1
    fi
    # Node resolves helper imports from their source directory, not the Git owner.
    # Reuse the verified materializer for code-only linked PR operations.
    if [ "$linked_matches_anchor" != "1" ] ||
      { is_locked_pr_command "$requested_subcommand" && [ ! -d "$script_parent_dir/../node_modules" ]; }; then
      wrapper_route_reason="differs from $anchor_description"
      if [ "$linked_matches_anchor" = "1" ]; then
        wrapper_route_reason="matches $anchor_description but has no node_modules directory"
      fi
      requested_classification=$(pr_subcommand_classification "$requested_subcommand" 2>/dev/null || true)
      if [ "$linked_matches_anchor" != "1" ] && [ "$dev_wrapper_opt_in" = "1" ] && [ "$requested_classification" = "advisory" ]; then
        if [ "${OPENCLAW_PR_DEV_WRAPPER_BANNER_SHOWN:-}" != "1" ]; then
          local_head_revision=$(pr_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 [ "$linked_matches_anchor" != "1" ] && [ "$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 [ "$script_self" != "$canonical_self" ] && [ -n "$anchor_wrapper_revision" ] &&
          [ "$canonical_wrapper_revision" = "$anchor_wrapper_revision" ] &&
          pr_git -C "$canonical_repo_root" diff --quiet HEAD -- "${anchor_components[@]/#/:(top)}"; then
          hold_pr_workflow_selection "$anchor_commit" "$canonical_repo_root" || exit 1
          echo "scripts/pr wrapper in this worktree $wrapper_route_reason; running the canonical checkout's wrapper (matches the $anchor_description trust anchor): $canonical_repo_root" >&2
          exec "$canonical_self" "$@"
        fi
        # The canonical checkout cannot supply trusted code and dependency context.
        # 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=$(pr_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
          # Hold the directory we created across exec, not an environment pathname.
          # FD 9 is reserved for the supervisor; bootstrap children are awaited.
          trap 'rm -rf "$anchor_exec_dir"' EXIT
          trap 'exit 129' HUP
          trap 'exit 130' INT
          trap 'exit 131' QUIT
          trap 'exit 143' TERM
          exec 9< "$anchor_exec_dir"
          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 pr_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"
            # 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.
            if pr_batch_wrapper_git verify "$anchor_git_root" "$anchor_commit" \
              "$anchor_exec_dir" "${anchor_components[@]}"; then
              anchor_extraction_valid=1
            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 selected installed tree.
            # Pin package directories: a later install may replace top-level aliases.
            # A whole node_modules link could load unanchored workspace source.
            anchor_dependency_owner="$anchor_exec_dir/scripts/pr-lib/materialize-dependencies.mjs"
            # Older anchors still rely on their parent to prepare dependencies.
            # Keep that bootstrap in this caller, using the extracted manifest.
            if [ ! -f "$anchor_dependency_owner" ]; then
              anchor_dependency_owner="$script_parent_dir/pr-lib/materialize-dependencies.mjs"
            fi
            if ! pr_materialize_dependencies "$anchor_dependency_owner" "$anchor_exec_dir/node_modules" "$anchor_commit"
            then
              exit 1
            fi
            # Bash re-reads this copy: successful exec hands cleanup to the
            # same creator PID, in anchored bash or its drained supervisor.
            # Older anchors without that handoff are reclaimed by the stale sweep.
            hold_pr_workflow_selection "$anchor_commit" "$anchor_exec_dir" || exit 1
            echo "scripts/pr wrapper in this worktree $wrapper_route_reason; running wrapper code materialized from the $anchor_reference trust anchor at revision $(pr_git -C "$script_parent_dir" rev-parse --short "$anchor_commit")." >&2
            # Bash 3.2 clears traps even when exec fails; errexit must be off
            # until we reinstall cleanup on the failure-only continuation.
            shopt -s execfail
            set +e
            OPENCLAW_PR_ANCHOR_REPO_ROOT="$canonical_repo_root" \
              OPENCLAW_PR_ANCHOR_CREATOR_PID="$$" OPENCLAW_PR_ANCHOR_FD=9 \
              exec "$anchor_exec_dir/scripts/pr" "$@"
            trap 'rm -rf "$anchor_exec_dir"' EXIT
            echo "Unable to execute the materialized scripts/pr wrapper." >&2
            exit 1
          fi
          rm -rf "$anchor_exec_dir"
          trap - EXIT HUP INT QUIT TERM
          exec 9<&-
        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.
        # An absent anchor differs at every path without launching another Git read.
        differing_wrapper_components=("${pr_wrapper_components[@]}")
        if [ -n "$anchor_commit" ] &&
          differing_components=$(pr_batch_wrapper_git differences "$script_parent_dir" \
            "$anchor_commit" "${pr_wrapper_components[@]}"); then
          differing_wrapper_components=()
          while IFS= read -r wrapper_component; do
            [ -z "$wrapper_component" ] || differing_wrapper_components+=("$wrapper_component")
          done <<< "$differing_components"
        fi
        if [ "$linked_matches_anchor" = "1" ]; then
          echo "scripts/pr wrapper matches origin/main, but its linked checkout has no node_modules directory." >&2
        else
          echo "scripts/pr implementation differs between this worktree and the canonical checkout, and does not match origin/main." >&2
        fi
        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 with installed dependencies whose wrapper matches the canonical checkout or a fetched origin/main." >&2
        exit 1
      fi
    fi
  fi
fi

# Keep the verified owner available after cleanup removes a linked wrapper.
canonical_repo_root="$(cd "$canonical_repo_root" && pwd -P)"
readonly canonical_repo_root

if [ -n "$PR_WORKFLOW_PR" ]; then
  # A pin is admission state, not caller-selected code. The operation lock
  # fences the verified closure and initial record before any PR side effect.
  PR_WORKFLOW_SOURCE_REVISION="${PR_WORKFLOW_SOURCE_REVISION:-${anchor_commit:-$(pr_git -C "$canonical_repo_root" rev-parse --verify refs/remotes/origin/main^{commit})}}"
  hold_pr_workflow_selection "$PR_WORKFLOW_SOURCE_REVISION" "$script_parent_dir/.." || exit 1
fi

is_main_only_pr_command() {
  case "$1" in
    prepare-init | prepare-correction-init | prepare-correction-continue | prepare-correction-review-init | prepare-baseline-refresh | prepare-baseline-successor | prepare-baseline-signing-request | prepare-baseline-signing-continue | 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 explicit body and
# evidence paths relative to the operator's caller before crossing that
# boundary; do not read them.
if [ "${1-}" = tooling-recover ]; then
  [ "$#" -ge 3 ] || { echo "tooling-recover requires a PR and plan" >&2; exit 2; }
  recovery_plan=$(node -e 'process.stdout.write(require("node:path").resolve(process.argv[1]))' -- "$3") || exit 1
  set -- "$1" "$2" "$recovery_plan" "${@:4}"
fi

if [ "${1-}" = merge-run ] || [ "${1-}" = merge-recover ] || [ "${1-}" = prepare-baseline-refresh ] || [ "${1-}" = prepare-baseline-successor ] || [ "${1-}" = prepare-baseline-signing-request ] || [ "${1-}" = prepare-baseline-signing-continue ]; 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
    if [ "$merge_cli_arg" = --body-file ] || [ "$merge_cli_arg" = --legacy-refusal ] || [ "$merge_cli_arg" = --pre-dispatch-refusal ] || [ "$merge_cli_arg" = --admin-evidence ] || [ "$merge_cli_arg" = --resolutions ] || [ "$merge_cli_arg" = --signed-commit ]; then merge_cli_body_next=true; fi
  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; }
    shopt -s execfail
    set +e
    exec node "$script_parent_dir/pr-lib/process-group-runner.mjs" "$canonical_repo_root" "$script_self" "$@"
    if [ "${OPENCLAW_PR_ANCHOR_CREATOR_PID:-}" = "$$" ] && [ "${OPENCLAW_PR_ANCHOR_FD:-}" = 9 ]; then
      trap 'node "$script_parent_dir/pr-lib/materialized-anchor.mjs" retire "$script_self" || true' EXIT
    fi
    echo "Unable to execute the scripts/pr supervisor." >&2
    exit 1
  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 workspace <PR>
    Report physical checkout/artifact paths before review-init; no PR state changes.
  scripts/pr gc [--pr <number>] [--dry-run]
  scripts/pr lock-recover <PR> <OWNER_OID> --confirmed-no-running-tools
  scripts/pr tooling-recover <PR> <plan> --plan-sha256 <SHA256> --confirmed-quiescent
  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-correction-init <PR>
  scripts/pr prepare-correction-continue <PR> --expected-head <SHA> --expected-tree <OID> --incoming-head <SHA> --incoming-review <OID> --publication-receipt <push|sync> --publication-oid <OID>
  scripts/pr prepare-correction-review-init <PR>
  scripts/pr prepare-baseline-refresh <PR> --expected-head <SHA> --baseline <SHA> [--resolutions <file>]
  scripts/pr prepare-baseline-successor <PR> --expected-head <SHA> --expected-tree <OID> --expected-review <OID> --predecessor-head <SHA> --predecessor-binding <OID> --baseline <SHA> [--resolutions <file>]
  scripts/pr prepare-baseline-signing-request <PR> --expected-head <SHA> --baseline <SHA> --signer <fingerprint> [--resolutions <file>] [successor identity options]
  scripts/pr prepare-baseline-signing-continue <PR> --request <OID> --signed-commit <file>
  scripts/pr prepare-validate-commit <PR>
  scripts/pr prepare-gates <PR>
  scripts/pr prepare-push <PR> [--resume-crabbox-run <Actions run ID>]
    Resume observes the selected protected publisher only; never pushes or dispatches.
  scripts/pr prepare-sync-head <PR>
  scripts/pr prepare-run <PR>
  scripts/pr ci-dispatch <PR> [--backend crabbox]
  scripts/pr merge-verify <PR>
  scripts/pr merge-run <PR> [--auto-merge] [--body-file <path>] [--admin-evidence <path> --confirmed-operator-admin]
    OPENCLAW_PR_MERGE_METHOD=merge|rebase preserves the PR commit series.
    --auto-merge selects pinned immediate squash for CLEAN, auto for BEHIND/BLOCKED (MERGEABLE only).
    OPENCLAW_PR_GATES_REMOTE=github prepare-run defers required CI to this auto-merge handoff.
    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 tree-changing PR commits or PR authorship are retained; known machine credit is excluded. Queue merges reject it.
    --admin-evidence admits reviewed prior-green conflict repairs or attributed pre-existing CI failures, with active organization-admin authority.
    Exact evidence is required; branch-caused failures, security checks, and enforced reviews remain blocking.
    This requires --confirmed-operator-admin and immediate squash without auto-merge.
    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> [--auto-merge]] [--current-workflow] [--merged-head <SHA>] [--cancel-auto [--observed-head <SHA>]] [--body-file <path>] [--legacy-refusal <evidence-directory>] [--pre-dispatch-refusal <evidence-directory>] [--admin-evidence <path> --confirmed-operator-admin]
    Explicitly authorize one new attempt after inspecting the retained outcome and remote history.
    Defaults to the retained head; replacement requires an explicitly selected full lowercase 40-character SHA,
    freshly authored exact-head review/preparation and completed CI proof. Standing landing authority covers
    a replacement repairing the same scope; new scope or another method needs authorization. Never rebases automatically.
    Ordinary recovery keeps prior evidence and requires the same method with immediate non-admin admission.
    Paired admin flags permit an explicitly selected head after confirmed auto cancellation, including the retained SHA.
    They also permit same-head recovery without --replacement-head for the complete known prior-CI REST base-modified HTTP 405 rejection.
    This reruns current admin admission and retains the original captures; other admin failures remain fenced.
    --current-workflow explicitly selects current official main for either cancelled-auto replacement or
    same-head qualified HTTP 405 admin recovery; the retained binding remains unchanged.
    --replacement-head <SHA> --auto-merge retires an unaccepted prior-CI REST admin intent fenced by a different
    exact current head, then requests ordinary current-head auto-merge. It requires exact-head github_pending
    preparation and all normal required-check gates; it never reuses prior admin evidence.
    --cancel-auto instead retires the exact accepted or uncertain non-queue auto request before head repair.
    Cancels a matching active request once, or records an already absent request without sending cancellation.
    --observed-head <SHA> selects a changed OPEN head whose accepted auto request is already absent.
    It records retirement only after stable observations; armed requests and concurrent drift stay refused.
    Repeated cancellation reconciles only. Use its current outcome OID for later reviewed/prepared recovery.
    --merged-head <SHA> records the receipt of an accepted auto request that GitHub merged on a pushed descendant
    head after inspecting the PR timeline. It requires the exact authoritative merged head and never dispatches.
  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.
workspace does not require gh, jq, rg, pnpm, or installed Node packages.
When macOS blocks ps, PR operation locks require Python 3 with ctypes on PATH.
USAGE
}

require_cmds() {
  local missing=()
  local cmd
  for cmd in 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
  pr_observe "$pr" || exit 1
  base_json="$PR_OBSERVATION"
  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) ;;
    workspace)
      [ "$#" -eq 1 ] && is_canonical_pr_number "$1" || { usage; exit 2; }
      # No GitHub tooling, dependency installation, or PR lock is needed to locate artifacts.
      show_pr_workspace "$1"
      return
      ;;
    gc)
      local gc_dry_run=false gc_pr=""
      while [ "$#" -gt 0 ]; do
        case "$1" in
          --dry-run)
            [ "$gc_dry_run" = false ] || { usage; exit 2; }
            gc_dry_run=true
            shift
            ;;
          --pr)
            [ -z "$gc_pr" ] && [ "$#" -ge 2 ] && is_canonical_pr_number "$2" || { usage; exit 2; }
            gc_pr="$2"
            shift 2
            ;;
          *) usage; exit 2 ;;
        esac
      done
      ;;
    tooling-recover)
      [ "$#" -eq 5 ] && is_canonical_pr_number "$1" && [ "$3" = --plan-sha256 ] &&
        [[ "$4" =~ ^[0-9a-f]{64}$ ]] && [ "$5" = --confirmed-quiescent ] || { 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="" merged_head="" observed_head="" replacement_head="" body_path="" legacy_directory="" cancel_auto=false refusal_directory="" admin_evidence="" confirmed_admin=false
      [ -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
          --current-workflow)
            [ "$cmd" = merge-recover ] && [ -n "$PR_WORKFLOW_RECOVERY_OUTCOME" ] || { usage; exit 2; }
            shift
            ;;
          --cancel-auto)
            [ "$cmd" = merge-recover ] && [ "$cancel_auto" = false ] || { usage; exit 2; }
            cancel_auto=true
            shift
            ;;
          --observed-head)
            [ "$cmd" = merge-recover ] && [ "$#" -ge 2 ] &&
              [[ "$2" =~ ^[0-9a-f]{40}$ ]] && [ -z "$observed_head" ] || { usage; exit 2; }
            observed_head="$2"
            shift 2
            ;;
          --admin-evidence)
            [ "$#" -ge 2 ] && [ -n "$2" ] && [ -z "$admin_evidence" ] || { usage; exit 2; }
            admin_evidence="$2"
            shift 2
            ;;
          --confirmed-operator-admin)
            [ "$confirmed_admin" = false ] || { usage; exit 2; }
            confirmed_admin=true
            shift
            ;;
          --auto-merge)
            [ "$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
            ;;
          --pre-dispatch-refusal)
            [ "$cmd" = merge-recover ] && [ "$#" -ge 2 ] && [ -n "$2" ] && [ -z "$refusal_directory" ] || { usage; exit 2; }
            refusal_directory="$2"
            shift 2
            ;;
          --legacy-refusal)
            [ "$cmd" = merge-recover ] && [ "$#" -ge 2 ] && [ -n "$2" ] && [ -z "$legacy_directory" ] || { usage; exit 2; }
            legacy_directory="$2"
            shift 2
            ;;
          --merged-head)
            [ "$cmd" = merge-recover ] && [ "$#" -ge 2 ] &&
              [[ "$2" =~ ^[0-9a-f]{40}$ ]] && [ -z "$merged_head" ] || { usage; exit 2; }
            merged_head="$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
      if [ -n "$admin_evidence" ] || [ "$confirmed_admin" = true ]; then
        [ -n "$admin_evidence" ] && [ "$confirmed_admin" = true ] && [ "$auto_merge" = false ] &&
          [ -z "$legacy_directory$refusal_directory" ] && [ "$cancel_auto" = false ] &&
          [ "${OPENCLAW_PR_MERGE_METHOD:-squash}" = squash ] || { usage; exit 2; }
      fi
      if [ "$cmd" = merge-recover ] && [ "$auto_merge" = true ]; then
        [ -n "$replacement_head" ] && [ -z "$body_path$legacy_directory$refusal_directory$admin_evidence" ] &&
          [ "$confirmed_admin" = false ] && [ "$cancel_auto" = false ] || { usage; exit 2; }
      fi
      if [ -n "$merged_head" ]; then
        [ -z "$replacement_head$body_path$legacy_directory$refusal_directory$admin_evidence" ] &&
          [ "$cancel_auto" = false ] && [ "$auto_merge" = false ] && [ "$confirmed_admin" = false ] || { usage; exit 2; }
      fi
      [ -z "$legacy_directory" ] || [ -n "$replacement_head" ] || { usage; exit 2; }
      [ -z "$refusal_directory" ] || [ -z "$legacy_directory" ] || { usage; exit 2; }
      [ "$cancel_auto" = false ] || [ -z "$replacement_head$body_path$legacy_directory$refusal_directory" ] || { usage; exit 2; }
      [ -z "$observed_head" ] || [ "$cancel_auto" = true ] || { usage; exit 2; }
      set -- "$merge_pr"
      ;;
    prepare-push)
      if [ "$#" -ne 1 ]; then
        [ "$#" -eq 3 ] && [ "$2" = --resume-crabbox-run ] &&
          [[ "$3" =~ ^[1-9][0-9]*$ ]] &&
          { [ "${#3}" -lt 16 ] || { [ "${#3}" -eq 16 ] && [[ "$3" < 9007199254740992 ]]; }; } || { usage; exit 2; }
      fi
      ;;
    ci-dispatch)
      [ "$#" -eq 1 ] || { [ "$#" -eq 3 ] && [ "$2" = --backend ] && [ "$3" = crabbox ]; } || { usage; exit 2; }
      ;;
    review-init | review-checkout-main | review-checkout-pr | review-claim | review-guard | review-artifacts-init | review-validate-artifacts | prepare-init | prepare-correction-init | prepare-correction-continue | prepare-correction-review-init | prepare-baseline-refresh | prepare-baseline-successor | prepare-baseline-signing-request | prepare-baseline-signing-continue | prepare-validate-commit | prepare-gates | prepare-sync-head | prepare-run | merge-verify)
      [ "$#" -ge 1 ] || { usage; exit 2; }
      ;;
    *)
      usage
      exit 2
      ;;
  esac

  # Reject contradictory instructions before PR reads, locks, or preparation can
  # retire valid evidence. prepare-push can refresh a head and rerun gates too.
  case "$cmd" in
    prepare-run | prepare-gates | prepare-push)
      resolve_pr_gates_remote_mode >/dev/null || return $?
      ;;
  esac

  require_cmds

  case "$cmd" in
    review-validate-artifacts) review_artifact_preflight "${1-}" || return 1 ;;
    prepare-init | prepare-run) review_artifact_preflight "${1-}" true || return 1 ;;
    prepare-correction-init) review_artifact_preflight "${1-}" correction || return 1 ;;
  esac

  # 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
    if [ "$PR_WORKFLOW_BOOTSTRAP" = 1 ] && [ "$cmd" = prepare-push ] && [ "${2:-}" = --resume-crabbox-run ]; then
      echo "Publisher resume requires an authenticated workflow source; restore its main tracking anchor before resuming. No acquisition attempted." >&2
      return 1
    fi
    [ -z "$PR_WORKFLOW_PR" ] || ensure_gh_api_auth
    admit_pr_workflow_binding "$locked_pr"
    trap 'exit 129' HUP
    trap 'exit 130' INT
    trap 'exit 131' QUIT
    trap 'exit 143' TERM
  fi

  case "$cmd" in
    tooling-recover)
      mark_pr_operation_side_effects_started
      node "$script_parent_dir/pr-lib/materialize-dependencies.mjs" --recover "$1" "$2" "$4" \
        "$(repo_root)" "$PR_OPERATION_LOCK_REF" "$PR_OPERATION_LOCK_OWNER_OID"
      ;;
    ls)
      list_pr_worktrees
      ;;
    gc)
      gc_pr_worktrees "$gc_dry_run" "$gc_pr"
      ;;
    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-correction-init)
      [ "$#" -eq 1 ] || { usage; exit 2; }
      prepare_init "$1" "${PR_OBSERVATION:-}" correction
      ;;
    prepare-baseline-refresh)
      prepare_baseline_refresh "$@"
      ;;
    prepare-baseline-successor)
      prepare_baseline_successor "$@"
      ;;
    prepare-baseline-signing-request)
      prepare_baseline_signing_request "$@"
      ;;
    prepare-baseline-signing-continue)
      prepare_baseline_signing_continue "$@"
      ;;
    prepare-correction-continue)
      prepare_correction_continue "$@"
      ;;
    prepare-correction-review-init)
      [ "$#" -eq 1 ] || { usage; exit 2; }
      prepare_correction_review_init "$1"
      ;;
    prepare-init)
      local pr="${1-}"
      [ -n "$pr" ] || { usage; exit 2; }
      prepare_init "$pr" "${PR_OBSERVATION:-}"
      ;;
    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" "" "${3:-}"
      ;;
    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" "${PR_OBSERVATION:-}"
      ;;
    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" '{"replacementHead":"","autoMergeRequested":false,"qualifiedRefusal":false,"observation":null}'
      ;;
    merge-run | merge-recover)
      merge_run "$merge_pr" "$auto_merge" "$recovery_oid" "$replacement_head" "$body_path" "$legacy_directory" "$cancel_auto" "$refusal_directory" "$admin_evidence" "$confirmed_admin" "$merged_head" "$observed_head"
      ;;
    merge-complete)
      merge_complete "$1" "$2"
      ;;
    *)
      usage
      exit 2
      ;;
  esac
}

main "$@"
