#!/usr/bin/env bash
# finish-branch.sh — the shared, tested "brain" for landing a finished feature branch.
# audience: AI coding agents first.
#
# WHY THIS EXISTS: landing a branch is the most destructive thing an agent does (publishes to
# main, removes worktrees, deletes branches). The old prose cascade let a subagent re-derive the
# git commands every time and blindly `-X theirs` — silently dropping work. This makes the
# mechanical, footgun-prone steps ONE set of fail-closed primitives invoked BY PATH, and hands
# every conflict to the AGENT (never the user) to resolve by judgment (see the conflict rubric).
#
# DIVISION OF LABOR:
#   - Script owns the FLOOR (deterministic + irreversible): cwd-correct git (every call `git -C`),
#     gated fast-forward (main only ever advances to a tree that has NO conflict markers, a clean
#     index, and PASSES the project test command), ordered cleanup (worktree-remove BEFORE
#     branch-delete), detect-and-abort on every learned footgun.
#   - Agent owns the CEILING (judgment): resolve each conflict per the rubric; decide whether the
#     test suite meaningfully covered the conflicted lines ("resolved but unverified at X").
#
# CONTRACT: each subcommand prints ONE line of JSON on stdout. A gate result (conflict, dirty tree,
#   tests-failed, blockers present) is DATA in that JSON with exit 0 — the agent reads `status`/
#   `ready` and acts. Exit non-zero ONLY on a usage/environment fault (bad args, not a git repo).
#   Diagnostics → stderr. FAIL-CLOSED: an unexpected state aborts to the agent, never guesses/forces.
#
# Resolve-on-branch invariant: conflicts are resolved on the BRANCH (in its worktree); main is only
#   ever fast-forwarded to an already-verified branch head. main never passes through a broken state.
#
# Subcommands:
#   preflight  <main> <branch> <wt>                 read-only; main-side pre-merge blockers
#   sync-base  <main> <branch> <base> <wt>          merge base INTO branch (resolve-on-branch)
#   land-merge <main> <branch> <base> <wt> <testcmd>  GATED ff of main to the verified branch head
#   land-pr    <main> <branch> <base> <wt> <testcmd>  push branch + open PR (never touches main)
#   deploy-preview <wt> <deploycmd>                 deploy branch to preview; capture .ship-preview-url
#   e2e-gate   <wt> <e2ecmd> <url>                  run e2e against the LIVE preview URL (pure gate)
#   cleanup    <main> <branch> <wt> [--keep-branch] [--no-push] [--assets-ok]
#   drift      --root R --base B --mode M [--anchor KIND:VAL]   validate frozen land facts (fail-closed)
#   land       --root R --base B --mode M [--anchor A] --testcmd T [--depcmd D]
#                  [--deploycmd C --e2ecmd C --promote pr|merge-to-main] -- <branch> <wt> [--assets-ok]
#              full per-project land ORCHESTRATOR (drift + mode-correct primitive cascade + ladder).
#              mode deploy-verify = deploy-preview → e2e-gate → _promote(--promote) on green.
#              The per-project ship.sh is now pure frozen DATA + one `land` delegation (no logic here
#              is duplicated per project). Exit 0 done / 20 agent-action-needed / 3 drift|usage.
set -uo pipefail

# Escape a string for a JSON double-quoted value (controlled, single-line).
jstr() { local s=${1//\\/\\\\}; s=${s//\"/\\\"}; s=${s//$'\t'/ }; s=${s//$'\n'/ }; printf '%s' "$s"; }

# Emit {"status":"<s>", <extra>} — <extra> is pre-formed JSON members (no leading comma) or empty.
emit_status() { local s=$1 extra=${2:-}; if [[ -n "$extra" ]]; then printf '{"status":"%s",%s}\n' "$s" "$extra"; else printf '{"status":"%s"}\n' "$s"; fi; }

# Agent-facing ladder line for the `land` orchestrator: {"stage","status","next","detail"}.
emit_ladder() { printf '{"stage":"%s","status":"%s","next":"%s","detail":"%s"}\n' "$1" "$2" "$3" "$(jstr "$4")"; }

# Extract a field from one line of the brain's OWN controlled single-line JSON (no python dep).
jq_status() { local s=$1; [[ "$s" =~ \"status\":\"([^\"]*)\" ]] && printf '%s' "${BASH_REMATCH[1]}"; }
jq_field()  { local s=$1 k=$2; [[ "$s" =~ \"$k\":\"([^\"]*)\" ]] && printf '%s' "${BASH_REMATCH[1]}"; }
ready_of()  { local s=$1; [[ "$s" =~ \"ready\":(true|false) ]] && printf '%s' "${BASH_REMATCH[1]}"; }

# git in a given dir, quietly. Usage: g <dir> <git-args...>
g() { git -C "$1" "${@:2}"; }

# Assert a path is a git working tree (has .git). Usage fault (exit 3) if not.
need_repo() { [[ -e "$1/.git" ]] || { echo "finish-branch: not a git working tree: $1" >&2; return 3; }; }

# List tracked files containing conflict markers in <wt>. Echoes newline-separated paths (may be empty).
conflict_marked() { git -C "$1" grep -lE '^(<<<<<<< |>>>>>>> )' -- . 2>/dev/null || true; }

# JSON array of porcelain-dirty TRACKED paths in <dir> (excludes untracked '??'). Echoes e.g. ["a","b"].
dirty_tracked_json() {
  git -C "$1" status --porcelain 2>/dev/null | awk '
    BEGIN{ printf "[" ; n=0 }
    { if (substr($0,1,2) != "??") { p=substr($0,4); if(n++) printf ","; printf "\"%s\"", p } }
    END{ printf "]" }'
}

run_test_gate() {
  local wt=$1 testcmd=$2 timeout_seconds=${FINISH_BRANCH_TEST_TIMEOUT_SECONDS:-1800}
  local gate=${FINISH_BRANCH_LOCAL_GATE:-$HOME/.claude/bin/local-gate} key
  [[ "$timeout_seconds" =~ ^[1-9][0-9]*$ ]] || return 125
  [[ -x "$gate" ]] || return 127
  key="finish-branch-$(printf '%s\0%s' "$wt" "$testcmd" | sha256sum | cut -c1-16)"
  ( cd "$wt" && timeout --foreground --kill-after=10s "${timeout_seconds}s" "$gate" --key "$key" --remote-env CI=true -- bash -c "$testcmd" )
}

# True when <pkg>/package.json declares a non-empty string scripts.typecheck.
_pkg_has_typecheck_script() {
  local pkg_json=$1
  python3 - "$pkg_json" <<'PY'
import json, sys
try:
    scripts = json.load(open(sys.argv[1], encoding="utf-8")).get("scripts") or {}
except Exception:
    sys.exit(2)
tc = scripts.get("typecheck")
sys.exit(0 if isinstance(tc, str) and tc.strip() else 1)
PY
}

# Nearest package boundary (package.json) at or above <rel>. A package.json IS the
# boundary: if it lacks scripts.typecheck the search stops (does not inherit a parent).
# Prints package dir (relative, or ".") and returns 0 when that package has typecheck;
# returns 1 when the boundary has no typecheck or no package.json exists.
_nearest_typecheck_pkg() {
  local root=$1 rel=$2 dir
  dir=$rel
  [[ -e "$root/$rel" && ! -d "$root/$rel" ]] && dir=$(dirname -- "$rel")
  [[ "$dir" == "." ]] && dir=""
  while :; do
    local pkg_dir="." json="$root/package.json"
    if [[ -n "$dir" ]]; then
      pkg_dir=$dir
      json="$root/$dir/package.json"
    fi
    if [[ -f "$json" ]]; then
      _pkg_has_typecheck_script "$json" || return 1
      printf '%s\n' "$pkg_dir"
      return 0
    fi
    [[ -z "$dir" || "$dir" == "." ]] && break
    dir=$(dirname -- "$dir")
    [[ "$dir" == "." ]] && dir=""
  done
  return 1
}

# Fail-closed typecheck of every package the diff touches that declares scripts.typecheck.
# Also: any changed *.ts/*.tsx/*.mts/*.cts that is not under such a package → RED (unverifiable).
# Missing tool, unreadable exit, timeout, or non-zero → RED. Prints a short reason on stderr.
# 0 = green (or nothing to check); non-zero = red.
_typecheck_touched_packages() {
  local c=$1 base=$2 head=$3
  local timeout_seconds=${FINISH_BRANCH_TYPECHECK_TIMEOUT_SECONDS:-600}
  local path pkg script out rc uncovered=0
  local -a pkgs=()
  declare -A seen=()

  [[ "$timeout_seconds" =~ ^[1-9][0-9]*$ ]] || {
    echo "typecheck: invalid FINISH_BRANCH_TYPECHECK_TIMEOUT_SECONDS" >&2; return 125; }

  local paths
  paths=$(git -C "$c" diff --name-only --diff-filter=ACMR "$base" "$head" 2>/dev/null) || {
    echo "typecheck: could not list $base..$head" >&2; return 1; }

  while IFS= read -r path; do
    [[ -n "$path" ]] || continue
    pkg=$(_nearest_typecheck_pkg "$c" "$path" 2>/dev/null) || pkg=""
    if [[ -n "$pkg" ]]; then
      [[ -n "${seen[$pkg]:-}" ]] && continue
      seen[$pkg]=1
      pkgs+=("$pkg")
      continue
    fi
    case "$path" in
      *.d.ts) ;;
      *.ts|*.tsx|*.mts|*.cts)
        echo "typecheck: changed TypeScript path has no package typecheck script: $path" >&2
        uncovered=1
        ;;
    esac
  done <<< "$paths"

  if [[ $uncovered -ne 0 ]]; then
    return 2
  fi
  ((${#pkgs[@]})) || return 0

  for pkg in "${pkgs[@]}"; do
    local pkg_json="$c/package.json" pkg_dir="$c"
    if [[ "$pkg" != "." ]]; then
      pkg_json="$c/$pkg/package.json"
      pkg_dir="$c/$pkg"
    fi
    script=$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1],encoding="utf-8"))["scripts"]["typecheck"])' "$pkg_json" 2>/dev/null) || {
      echo "typecheck: unreadable typecheck script in $pkg" >&2; return 1; }
    [[ -n "$script" ]] || { echo "typecheck: empty typecheck script in $pkg" >&2; return 1; }
    echo "finish-branch: typecheck $pkg (timeout ${timeout_seconds}s)" >&2
    out=$( cd "$pkg_dir" && PATH="$pkg_dir/node_modules/.bin:$PATH" timeout --foreground --kill-after=10s "${timeout_seconds}s" bash -c "$script" 2>&1 ); rc=$?
    if [[ $rc -eq 124 || $rc -eq 137 ]]; then
      echo "typecheck: timed out in $pkg after ${timeout_seconds}s" >&2
      printf '%s\n' "$out" | tail -c 400 >&2
      return 124
    fi
    if [[ $rc -ne 0 ]]; then
      echo "typecheck: failed in $pkg (exit $rc)" >&2
      printf '%s\n' "$out" | tail -c 800 >&2
      return "$rc"
    fi
  done
  return 0
}

# ── preflight: main-side blockers that must be cleared (by the AGENT) before any land ─────────
# Read-only. Emits {"ready":bool,"blockers":[{"name","detail","recovery"}]}.
preflight() {
  local main=$1 branch=$2 wt=$3
  need_repo "$main" || return 3
  local N=() D=() R=()
  add() { N+=("$1"); D+=("$2"); R+=("$3"); }

  # pending-merge-on-main: an in-progress/abandoned merge blocks checkout & re-merge.
  if git -C "$main" rev-parse -q --verify MERGE_HEAD >/dev/null 2>&1; then
    local u; u=$(git -C "$main" diff --name-only --diff-filter=U 2>/dev/null | tr '\n' ' ')
    add pending-merge-on-main "main has an in-progress merge (MERGE_HEAD present); unmerged: ${u:-none}" \
        "complete or abort it: resolve each unmerged path, git -C <main> add, git -C <main> commit --no-edit (or git -C <main> merge --abort) before landing"
  fi

  # dirty-main-worktree: modified/staged TRACKED files on main (WIP or implementer contamination).
  local dirty; dirty=$(dirty_tracked_json "$main")
  if [[ "$dirty" != "[]" ]]; then
    add dirty-main-worktree "main working tree has uncommitted tracked changes: $dirty" \
        "HALT: the shared main checkout has uncommitted work that may belong to a live session. Landing will NOT touch it — this process never stashes, discards (checkout --), or resets the shared main tree. The OWNER of that WIP must commit it on a branch or clear it themselves before landing."
  fi

  local ready=true i; ((${#N[@]})) && ready=false
  local out='{"ready":'"$ready"',"blockers":['
  for ((i=0;i<${#N[@]};i++)); do
    [[ $i -gt 0 ]] && out+=','
    out+='{"name":"'"$(jstr "${N[$i]}")"'","detail":"'"$(jstr "${D[$i]}")"'","recovery":"'"$(jstr "${R[$i]}")"'"}'
  done
  out+=']}'
  printf '%s\n' "$out"
}

# ── sync-base: merge <base> INTO <branch> in its worktree (resolve-on-branch) ─────────────────
# Brings the branch current with base so a later land is a clean fast-forward. Conflicts are LEFT
# in the worktree for the AGENT to resolve per the rubric (then add+commit), NEVER auto-picked.
# Emits {"status":"clean"|"conflict"|"up-to-date","files":[...]}.
sync-base() {
  local main=$1 branch=$2 base=$3 wt=$4
  need_repo "$wt" || return 3
  # Worktree must have the branch checked out (resolve-on-branch happens here, not on main).
  local head; head=$(git -C "$wt" symbolic-ref --quiet --short HEAD 2>/dev/null || echo "")
  [[ "$head" == "$branch" ]] || { echo "finish-branch sync-base: worktree HEAD is '$head', expected branch '$branch'" >&2; return 3; }
  git -C "$wt" rev-parse -q --verify "$base^{commit}" >/dev/null 2>&1 || { echo "finish-branch sync-base: base does not resolve: $base" >&2; return 3; }

  if git -C "$wt" merge-base --is-ancestor "$base" "$branch" 2>/dev/null; then
    emit_status up-to-date '"files":[]'; return 0
  fi
  if git -C "$wt" merge --no-edit "$base" >/dev/null 2>&1; then
    emit_status clean '"files":[]'; return 0
  fi
  # Non-clean: surface conflicted paths and LEAVE the merge in progress for the agent.
  local files; files=$(git -C "$wt" diff --name-only --diff-filter=U 2>/dev/null \
    | awk 'BEGIN{printf "["}{if(n++)printf ",";printf "\"%s\"",$0}END{printf "]"}')
  emit_status conflict '"files":'"$files"
}

# ── land-merge: GATED fast-forward of main to a VERIFIED branch head ──────────────────────────
# Mechanical floor for the user's guarantee "main only ever advances to a tested commit":
#   1) no conflict markers in the branch worktree   2) clean index (no in-progress/unresolved merge)
#   3) base is an ancestor of branch head (ff is actually possible)   4) project test command passes.
# Only when ALL hold does main fast-forward. Any failure -> abort to the AGENT as data (exit 0).
land-merge() {
  local main=$1 branch=$2 base=$3 wt=$4 testcmd=$5
  need_repo "$main" || return 3; need_repo "$wt" || return 3

  local marked; marked=$(conflict_marked "$wt")
  if [[ -n "$marked" ]]; then
    local f; f=$(printf '%s' "$marked" | awk 'BEGIN{printf "["}{if(n++)printf ",";printf "\"%s\"",$0}END{printf "]"}')
    emit_status conflict-markers '"detail":"unresolved conflict markers remain — agent must finish resolving per the rubric","files":'"$f"; return 0
  fi
  local dirty; dirty=$(dirty_tracked_json "$wt")
  if [[ "$dirty" != "[]" ]]; then
    emit_status dirty-tree '"detail":"branch worktree has an uncommitted/unresolved index — commit the resolution first","files":'"$dirty"; return 0
  fi
  if ! git -C "$main" merge-base --is-ancestor "$base" "$branch" 2>/dev/null; then
    emit_status not-ff '"detail":"base is not an ancestor of branch — run sync-base first to make the land a clean fast-forward"'; return 0
  fi
  # Typecheck floor — independent of testcmd. A green test suite must never mask a tsc red.
  local tclog tc_rc detail
  echo "finish-branch: typecheck gate running" >&2
  tclog=$(_typecheck_touched_packages "$wt" "$base" "$branch" 2>&1); tc_rc=$?
  if [[ $tc_rc -ne 0 ]]; then
    detail="typecheck exited $tc_rc — do NOT land"
    [[ $tc_rc -eq 124 || $tc_rc -eq 137 ]] && detail="typecheck timed out — do NOT land"
    emit_status typecheck-failed '"detail":"'"$(jstr "$detail")"'","tail":"'"$(jstr "$(printf '%s' "$tclog" | tail -c 400)")"'"'; return 0
  fi
  # Test gate — the FLOOR. Runs even when sync reported clean (a marker-free merge can still be
  # semantically broken). Ceiling (did tests cover the conflicted lines?) stays with the agent.
  local tlog rc
  echo "finish-branch: test gate running (timeout ${FINISH_BRANCH_TEST_TIMEOUT_SECONDS:-1800}s)" >&2
  tlog=$(run_test_gate "$wt" "$testcmd" 2>&1); rc=$?
  if [[ $rc -ne 0 ]]; then
    local tail; tail=$(printf '%s' "$tlog" | tail -c 400)
    detail="project test command exited $rc — resolution is wrong or incomplete; do NOT land"
    [[ $rc -eq 124 || $rc -eq 137 ]] && detail="project test command timed out after ${FINISH_BRANCH_TEST_TIMEOUT_SECONDS:-1800}s — do NOT land"
    emit_status tests-failed '"detail":"'"$(jstr "$detail")"'","tail":"'"$(jstr "$tail")"'"'; return 0
  fi
  # All gates green -> fast-forward main to the verified head. ff-only can never rewrite main.
  if ! git -C "$main" merge --ff-only "$branch" >/dev/null 2>&1; then
    emit_status ff-failed '"detail":"git merge --ff-only refused — main moved or is dirty; re-run preflight"'; return 0
  fi
  local sha; sha=$(git -C "$main" rev-parse --short HEAD 2>/dev/null)
  emit_status landed '"base":"'"$(jstr "$base")"'","head":"'"$(jstr "$sha")"'"'
}

# ── land-pr: push branch + open a PR; NEVER touches main ──────────────────────────────────────
# Same test FLOOR as land-merge, minus the ff/ancestor check (a PR need not be ff). Deliberately
# does NOT sync base into the branch — pre-merging pollutes the PR diff with merge commits; sync
# only when the branch is actually unmergeable, as a separate explicit step.
# Fail-closed: no remote / no gh / not authed / PR already open -> abort to agent.
land-pr() {
  local main=$1 branch=$2 base=$3 wt=$4 testcmd=$5
  need_repo "$main" || return 3; need_repo "$wt" || return 3

  local marked; marked=$(conflict_marked "$wt")
  [[ -n "$marked" ]] && { emit_status conflict-markers '"detail":"unresolved conflict markers remain — resolve per the rubric before opening a PR"'; return 0; }
  local dirty; dirty=$(dirty_tracked_json "$wt")
  [[ "$dirty" != "[]" ]] && { emit_status dirty-tree '"detail":"commit the branch worktree before opening a PR","files":'"$dirty"; return 0; }
  local origin; origin=$(git -C "$main" remote get-url origin 2>/dev/null) || { emit_status no-remote '"detail":"no origin remote — cannot open a PR; add a remote or use merge-land"'; return 0; }
  command -v gh >/dev/null 2>&1 || { emit_status no-gh '"detail":"gh CLI not installed — cannot open a PR"'; return 0; }
  gh auth status >/dev/null 2>&1 || { emit_status gh-unauthed '"detail":"gh not authenticated — run: gh auth login"'; return 0; }
  # gh resolves the target repo AND (--fill) reads local git refs from its CWD. The orchestrator runs
  # land-pr from the AGENT's cwd (any repo, incl. an unrelated one) — so EVERY gh call must run inside
  # $main: there the repo auto-detects, -R pins it explicitly, and --fill's `base...branch` refs both
  # resolve (the branch ref is shared from its worktree). Running from elsewhere fails "ambiguous
  # argument 'base...branch'" or silently targets the wrong repo.
  local slug; slug=$(printf '%s' "$origin" | sed -E 's#^.*github\.com[:/]##; s#\.git$##')
  [[ -n "$slug" ]] || { emit_status no-remote '"detail":"origin is not a github.com remote — cannot open a PR via gh"'; return 0; }

  local existing ghlog ghrc gh_timeout=${FINISH_BRANCH_GH_TIMEOUT_SECONDS:-30}
  [[ "$gh_timeout" =~ ^[1-9][0-9]*$ ]] || { emit_status pr-list-failed '"detail":"FINISH_BRANCH_GH_TIMEOUT_SECONDS must be a positive integer"'; return 0; }
  ghlog=$(mktemp /tmp/finish-branch-gh-XXXXXX) || { emit_status pr-list-failed '"detail":"could not create gh diagnostic log"'; return 0; }
  existing=$( cd "$main" && timeout --foreground --kill-after=5s "${gh_timeout}s" gh pr list -R "$slug" --head "$branch" --state open --json number --jq '.[0].number' 2>"$ghlog" ); ghrc=$?
  if [[ $ghrc -ne 0 ]]; then
    local ghtail; ghtail=$(tail -c 400 "$ghlog" 2>/dev/null); rm -f "$ghlog"
    [[ $ghrc -eq 124 || $ghrc -eq 137 ]] \
      && emit_status pr-list-failed '"detail":"gh pr list timed out after '"$gh_timeout"'s; landing stopped before tests or push","tail":"'"$(jstr "$ghtail")"'"' \
      || emit_status pr-list-failed '"detail":"gh pr list exited '"$ghrc"'; landing stopped before tests or push","tail":"'"$(jstr "$ghtail")"'"'
    return 0
  fi
  rm -f "$ghlog"
  # Existing PRs still need the same gate and branch push so rerunning land publishes new commits.

  local tclog tc_rc tlog rc detail
  echo "finish-branch: typecheck gate running" >&2
  tclog=$(_typecheck_touched_packages "$wt" "$base" "$branch" 2>&1); tc_rc=$?
  if [[ $tc_rc -ne 0 ]]; then
    detail="typecheck exited $tc_rc — fix before opening a PR"
    [[ $tc_rc -eq 124 || $tc_rc -eq 137 ]] && detail="typecheck timed out — fix before opening a PR"
    emit_status typecheck-failed '"detail":"'"$(jstr "$detail")"'","tail":"'"$(jstr "$(printf '%s' "$tclog" | tail -c 400)")"'"'; return 0
  fi
  echo "finish-branch: test gate running (timeout ${FINISH_BRANCH_TEST_TIMEOUT_SECONDS:-1800}s)" >&2
  tlog=$(run_test_gate "$wt" "$testcmd" 2>&1); rc=$?
  if [[ $rc -ne 0 ]]; then
    detail="tests exited $rc — fix before opening a PR"
    [[ $rc -eq 124 || $rc -eq 137 ]] && detail="tests timed out after ${FINISH_BRANCH_TEST_TIMEOUT_SECONDS:-1800}s — fix before opening a PR"
    emit_status tests-failed '"detail":"'"$(jstr "$detail")"'","tail":"'"$(jstr "$(printf '%s' "$tlog" | tail -c 400)")"'"'; return 0
  fi

  git -C "$main" push -u origin "$branch" >/dev/null 2>&1 || { emit_status push-failed '"detail":"git push -u origin '"$branch"' failed — see stderr"'; return 0; }
  [[ -n "$existing" ]] && { emit_status pr-exists '"number":'"$existing"; return 0; }
  local url; url=$( cd "$main" && gh pr create -R "$slug" --base "$base" --head "$branch" --fill 2>/dev/null ) || { emit_status pr-create-failed '"detail":"gh pr create failed — branch is pushed; open the PR manually"'; return 0; }
  emit_status pr-opened '"url":"'"$(jstr "$url")"'"'
}

# ── deploy-preview: deploy the branch to a preview env, capture the live URL ──────────────────
# Runs <deploycmd> inside the branch worktree. Contract: deploycmd writes the reachable preview URL
# (single line) to $wt/.ship-preview-url. Fail-closed: a non-zero deploy OR a missing/empty URL file
# aborts to the agent — we NEVER run the e2e gate (and never promote) against a build we can't address.
# Emits {"status":"deployed","url":...} | {"status":"deploy-failed","tail":...} | {"status":"no-url",...}.
deploy-preview() {
  local wt=$1 deploycmd=$2
  need_repo "$wt" || return 3
  local urlfile="$wt/.ship-preview-url"; rm -f "$urlfile" 2>/dev/null || true
  local dlog rc; dlog=$( ( cd "$wt" && bash -c "$deploycmd" ) 2>&1 ); rc=$?
  if [[ $rc -ne 0 ]]; then
    emit_status deploy-failed '"detail":"deploy command exited '"$rc"'","tail":"'"$(jstr "$(printf '%s' "$dlog" | tail -c 400)")"'"'; return 0
  fi
  local url=""; [[ -f "$urlfile" ]] && url=$(head -n1 "$urlfile" 2>/dev/null | tr -d '[:space:]')
  if [[ -z "$url" ]]; then
    emit_status no-url '"detail":"deploy exited 0 but wrote no preview URL to .ship-preview-url — the deploy command must echo the reachable URL to that file; cannot verify or promote"'; return 0
  fi
  emit_status deployed '"url":"'"$(jstr "$url")"'"'
}

# ── e2e-gate: run the project e2e suite against the LIVE preview URL ───────────────────────────
# Runs <e2ecmd> in the worktree with PREVIEW_URL exported. Pure verification gate — mutates nothing.
# Emits {"status":"passed"} | {"status":"failed","tail":...}.
e2e-gate() {
  local wt=$1 e2ecmd=$2 url=$3
  need_repo "$wt" || return 3
  [[ -n "$url" ]] || { emit_status failed '"detail":"no preview URL supplied to e2e-gate"'; return 0; }
  local elog rc; elog=$( ( cd "$wt" && PREVIEW_URL="$url" bash -c "$e2ecmd" ) 2>&1 ); rc=$?
  if [[ $rc -ne 0 ]]; then
    emit_status failed '"detail":"e2e exited '"$rc"' against '"$(jstr "$url")"'","tail":"'"$(jstr "$(printf '%s' "$elog" | tail -c 400)")"'"'; return 0
  fi
  emit_status passed '"url":"'"$(jstr "$url")"'"'
}

# ── cleanup: ordered, asset-safe teardown ────────────────────────────────────────────────────
# Order is load-bearing: push (optional) -> worktree remove --force -> branch -d. Removing the
# worktree before deleting the branch avoids "branch is used by worktree"; removing it is
# IRREVERSIBLE, so gitignored assets (test suites, caches) are detected and the agent must preserve
# them first (re-run with --assets-ok). Flags let PR mode keep the branch and skip the main push.
cleanup() {
  local main=$1 branch=$2 wt=$3; shift 3
  local keep_branch=0 no_push=0 assets_ok=0
  while [[ $# -gt 0 ]]; do case $1 in
    --keep-branch) keep_branch=1;; --no-push) no_push=1;; --assets-ok) assets_ok=1;;
    *) echo "finish-branch cleanup: unknown flag: $1" >&2; return 3;; esac; shift; done
  need_repo "$main" || return 3

  # Irreversible-removal guard: expensive gitignored assets live ONLY in the worktree.
  if [[ "$assets_ok" -eq 0 && -d "$wt" ]]; then
    local found=()
    local p; for p in tmp .cache .turbo dist coverage; do
      [[ -d "$wt/$p" ]] && [[ -n "$(ls -A "$wt/$p" 2>/dev/null)" ]] && found+=("$p")
    done
    if ((${#found[@]})); then
      local fj; fj=$(printf '%s\n' "${found[@]}" | awk 'BEGIN{printf "["}{if(n++)printf ",";printf "\"%s\"",$0}END{printf "]"}')
      emit_status assets-present '"paths":'"$fj"',"recovery":"these gitignored dirs exist ONLY in the worktree and removal is irreversible — copy anything worth keeping to <main>/tmp, then re-run cleanup with --assets-ok"'
      return 0
    fi
  fi

  if [[ "$no_push" -eq 0 ]]; then
    git -C "$main" push >/dev/null 2>&1 || { emit_status push-failed '"detail":"git -C <main> push failed — main is landed locally but not pushed; resolve and re-run cleanup --no-push? no: push then re-run"'; return 0; }
  fi
  if [[ -d "$wt" ]]; then
    git -C "$main" worktree remove "$wt" --force >/dev/null 2>&1 || { emit_status worktree-remove-failed '"detail":"git worktree remove --force failed for '"$(jstr "$wt")"'"'; return 0; }
  fi
  if [[ "$keep_branch" -eq 0 ]]; then
    git -C "$main" branch -d "$branch" >/dev/null 2>&1 || { emit_status branch-delete-failed '"detail":"git branch -d '"$branch"' failed — not fully merged? use -D only after confirming it landed"'; return 0; }
  fi
  # run-plan liveness beacons: clear on clean land (no-op for the ship-skill path — files absent).
  if [[ "$branch" == plan/* ]]; then
    local _rp="${RUN_PLAN_RUN_DIR:-$HOME/.claude/workflows/.run}" _slug=${branch#plan/}
    rm -f "$_rp/$_slug.owner" "$_rp/$_slug.live" "$_rp/$_slug.pid" 2>/dev/null || true
  fi
  emit_status cleaned '"branch_kept":'"$([[ $keep_branch -eq 1 ]] && echo true || echo false)"
}

# ── docs lane: the gate's cost is DERIVED from the diff, never asserted ───────────────────────
# Every path in base..candidate inside the docs allowlist → docs checks only, build/test skipped.
# ONE path outside it, one unclassifiable path, one missing check binary, one dead canary → the FULL
# gate. There is deliberately no override flag: a bypass flag is fail-open by construction.
# Each check emits a stamp {gate,inputs_hashed,verdict,applicability,tool_version,canary_detected};
# verdict is pass|fail|could-not-run and a deliberate skip is could-not-run + not-applicable +
# discharged_by, carrying the SAME inputs_hashed as the classification that discharged it.
DOCS_LANE_VERSION="finish-branch-docs-lane/1"
DOCS_LANE_REASON=""
DOCS_LANE_HASH="-"
DOCS_LANE_FRONTMATTER=0

_docs_lane_stamp() { # <stampfile> <gate> <hash> <verdict> <applicability> <tool_version> <canary> [extra]
  printf '{"gate":"%s","inputs_hashed":"%s","verdict":"%s","applicability":"%s","tool_version":"%s","canary_detected":%s%s}\n' \
    "$(jstr "$2")" "$(jstr "$3")" "$(jstr "$4")" "$(jstr "$5")" "$(jstr "$6")" "$7" "${8:-}" >> "$1" 2>/dev/null || true
}

# 0 = inside the docs allowlist, 1 = forces the full gate. Deny rules are evaluated FIRST and win:
# anything the harness reads as instructions is executable in effect, whatever its extension.
_docs_lane_path_verdict() {
  local p=$1 seg oldifs; local -a segs
  [[ -n "$p" ]] || return 1
  [[ "$p" == /* ]] && return 1
  [[ "$p" == *".."* ]] && return 1
  case "$p" in *$'\n'*|*$'\r'*|*$'\t'*) return 1;; esac
  # A glob metacharacter makes a path argument a PATTERN that need not match its own file, so a
  # linter would report success having read nothing.
  case "$p" in *'*'*|*'?'*|*'['*|*']'*|*'{'*|*'}'*|*'!'*|*'('*|*')'*|*'\'*) return 1;; esac
  # A leading dash reaches a checker as a flag, not a file.
  case "$p" in -*|*/-*) return 1;; esac
  case "$p" in docs/plans/*|*/docs/plans/*) return 1;; esac
  case "$p" in
    *.sh|*.bash|*.zsh|*.mjs|*.cjs|*.js|*.jsx|*.ts|*.tsx|*.py|*.rb|*.pl|*.ps1|*.yml|*.yaml|*.json|*.jsonc|*.toml) return 1;;
  esac
  oldifs=$IFS; IFS=/ read -ra segs <<<"$p"; IFS=$oldifs
  for seg in "${segs[@]}"; do
    case "$seg" in
      .claude|.github|packaging|node_modules|.gitattributes|.gitignore|.gitleaksignore|.gitmodules) return 1;;
    esac
  done
  [[ "$p" == *.md ]] && return 0
  [[ "$p" == docs/* ]] && return 0
  return 1
}

# 0 = an agent/skill prompt file: docs by extension, but it changes what RUNS, so it still gets the
# secret scan AND a frontmatter schema check.
# The loaded entry files only: `<...>/agents/<name>.md` and `<...>/skills/<name>/SKILL.md`. Reference
# and example pages living beside them carry no frontmatter and must not be schema-checked.
_docs_lane_is_prompt_path() {
  local p=$1 dir
  [[ "$p" == *.md ]] || return 1
  [[ "${p##*/}" == "SKILL.md" ]] && return 0
  dir=${p%/*}; [[ "$dir" != "$p" && "${dir##*/}" == "agents" ]] && return 0
  return 1
}

# Classify base..head IN THE CANDIDATE (that delta is exactly what main gains). Sets DOCS_LANE_HASH,
# DOCS_LANE_REASON, DOCS_LANE_FRONTMATTER; writes surviving (non-deleted) paths NUL-delimited to
# <pathfile>. Returns 0 docs-lane-eligible, 1 full gate.
_docs_lane_classify() {
  local c=$1 base=$2 head=$3 pathfile=$4
  local rawfile normfile rec path smode dmode ssha dsha status n=0 verdict=0
  DOCS_LANE_HASH="-"; DOCS_LANE_REASON=""; DOCS_LANE_FRONTMATTER=0
  : > "$pathfile" 2>/dev/null || { DOCS_LANE_REASON="cannot write classification path list"; return 1; }
  rawfile=$(mktemp "$c/.git/docs-lane-raw-XXXXXX") || { DOCS_LANE_REASON="cannot create diff buffer"; return 1; }
  normfile=$(mktemp "$c/.git/docs-lane-norm-XXXXXX") || { rm -f "$rawfile"; DOCS_LANE_REASON="cannot create diff buffer"; return 1; }
  if ! git -C "$c" diff --raw -z --no-renames "$base" "$head" > "$rawfile" 2>/dev/null; then
    rm -f "$rawfile" "$normfile"; DOCS_LANE_REASON="could not compute $base..$head"; return 1
  fi
  while IFS= read -r -d '' rec; do
    if ! IFS= read -r -d '' path; then
      verdict=1; DOCS_LANE_REASON="truncated diff record"; break
    fi
    rec=${rec#:}
    read -r smode dmode ssha dsha status <<<"$rec"
    n=$((n+1))
    printf '%s %s %s %s\n' "$smode" "$dmode" "$status" "$path" >> "$normfile"
    if [[ "$smode" == 120000 || "$dmode" == 120000 || "$smode" == 160000 || "$dmode" == 160000 ]]; then
      verdict=1; DOCS_LANE_REASON="symlink/gitlink entry ($path)"; continue
    fi
    case "$status" in
      A*|M*|D*) ;;
      *) verdict=1; DOCS_LANE_REASON="unhandled diff status $status ($path)"; continue;;
    esac
    if ! _docs_lane_path_verdict "$path"; then
      verdict=1; [[ -n "$DOCS_LANE_REASON" ]] || DOCS_LANE_REASON="path outside the docs allowlist ($path)"; continue
    fi
    _docs_lane_is_prompt_path "$path" && DOCS_LANE_FRONTMATTER=1
    [[ "$status" == D* ]] || printf '%s\0' "$path" >> "$pathfile"
  done < "$rawfile"
  DOCS_LANE_HASH=$(sort "$normfile" 2>/dev/null | sha256sum 2>/dev/null | cut -c1-32)
  [[ -n "$DOCS_LANE_HASH" ]] || DOCS_LANE_HASH="-"
  rm -f "$rawfile" "$normfile"
  if [[ $n -eq 0 ]]; then DOCS_LANE_REASON="empty diff"; return 1; fi
  return $verdict
}

# The classifier's canary: it must demonstrate it can say NO, not only YES. A classifier that only
# ever proves it can allow is the same meaningless green as a secret scan that never fires.
_docs_lane_classifier_canary() {
  _docs_lane_path_verdict "docs/__canary__.md" || return 1
  _docs_lane_path_verdict "src/__canary__.ts" && return 1
  _docs_lane_path_verdict "docs/__canary__.sh" && return 1
  _docs_lane_path_verdict "docs/plans/__canary__.md" && return 1
  _docs_lane_path_verdict ".claude/agents/__canary__.md" && return 1
  _docs_lane_path_verdict "docs/../src/__canary__.md" && return 1
  _docs_lane_is_prompt_path "modules/x/agents/__canary__.md" || return 1
  _docs_lane_is_prompt_path "modules/x/skills/y/SKILL.md" || return 1
  _docs_lane_is_prompt_path "modules/x/skills/y/references/__canary__.md" && return 1
  _docs_lane_is_prompt_path "docs/__canary__.md" && return 1
  return 0
}

_docs_lane_mdlint_config() { # <dir> <relative-links-rule-path>
  cat > "$1/mdlint.jsonc" <<'EOF'
{"config":{"default":false,"MD011":true,"MD039":true,"MD042":true,"MD051":true,"MD052":true,"MD053":true,"MD056":true},"globs":[],"noProgress":true}
EOF
  printf '{"config":{"default":false,"relative-links":true},"customRules":["%s"],"globs":[],"noProgress":true}\n' "$2" > "$1/mdlink.jsonc"
}

_docs_lane_frontmatter_check() { # <file>... ; prints violations, rc 1 when any
  python3 - "$@" <<'EOF'
import sys
try:
    import yaml
except Exception:
    print("python yaml module unavailable"); sys.exit(2)
EFFORT = {"low", "medium", "high", "xhigh", "max", "med"}
bad = []
for p in sys.argv[1:]:
    try:
        text = open(p, encoding="utf-8").read()
    except Exception:
        bad.append(f"{p}: unreadable"); continue
    lines = text.split("\n")
    # No frontmatter block asserts nothing about what runs; only a block that EXISTS is validated.
    if not lines or lines[0].strip() != "---":
        continue
    close = next((i for i in range(1, len(lines)) if lines[i].strip() == "---"), None)
    if close is None:
        bad.append(f"{p}: unterminated frontmatter"); continue
    try:
        fm = yaml.safe_load("\n".join(lines[1:close]))
    except Exception:
        bad.append(f"{p}: invalid YAML frontmatter"); continue
    if not isinstance(fm, dict):
        bad.append(f"{p}: frontmatter is not a mapping"); continue
    name = fm.get("name")
    if not isinstance(name, str) or not name.strip():
        bad.append(f"{p}: missing or empty 'name'")
    model = fm.get("model")
    if model is not None and (not isinstance(model, str) or not model.strip()):
        bad.append(f"{p}: 'model' must be a non-empty string")
    effort = fm.get("effort")
    if effort is not None:
        ok = (isinstance(effort, int) and not isinstance(effort, bool)) \
            or (isinstance(effort, str) and (effort.strip().lower() in EFFORT or effort.strip().lstrip("-").isdigit()))
        if not ok:
            bad.append(f"{p}: invalid 'effort' {effort!r}")
for b in bad:
    print(b)
sys.exit(1 if bad else 0)
EOF
}

# Run the docs lane. 0 = green (land), 1 = cannot run the lane (caller MUST run the full gate),
# 2 = a docs check went red (do NOT land). Never returns 0 on an unproven check.
_docs_lane_gate() {
  local c=$1 stamps=$2 hash=$3 pathfile=$4 want_fm=$5
  local work gitleaks mdcli discharge out rc n=0
  local -a paths=() mds=() prompts=()
  gitleaks=${FINISH_BRANCH_GITLEAKS:-gitleaks}
  mdcli=${FINISH_BRANCH_MARKDOWNLINT:-$c/node_modules/.bin/markdownlint-cli2}
  discharge=',"discharged_by":"path-classification"'

  while IFS= read -r -d '' p; do
    paths+=("$p"); n=$((n+1))
    [[ "$p" == *.md ]] && mds+=("$p")
    _docs_lane_is_prompt_path "$p" && prompts+=("$p")
  done < "$pathfile"

  work=$(mktemp -d "$c/.git/docs-lane-XXXXXX") || { DOCS_LANE_REASON="cannot create docs-lane workspace"; return 1; }
  _docs_lane_mdlint_config "$work" "$(cd "$(dirname "$mdcli")/.." 2>/dev/null && pwd)/markdownlint-rule-relative-links"
  printf '# canary\n\n(reversed)[http://example.invalid]\n\n[missing](./__no_such_file__.md)\n' > "$work/canary.md"
  printf -- '---\nname: canary\neffort: __not_a_valid_effort__\n---\n\nbody\n' > "$work/canary-frontmatter.md"
  mkdir -p "$work/secret" && printf 'aws_key = "AKIA%s"\n' "2E0A8F3B244C9986" > "$work/secret/canary.txt"

  if ! command -v "$gitleaks" >/dev/null 2>&1; then
    _docs_lane_stamp "$stamps" secret-scan "$hash" could-not-run applicable "gitleaks/absent" false
    DOCS_LANE_REASON="gitleaks not available"; rm -rf "$work"; return 1
  fi
  local gl_version; gl_version="gitleaks/$( "$gitleaks" version 2>/dev/null | tr -d '\n' )"
  ( cd "$work" && "$gitleaks" detect --no-git --source "$work/secret" --no-banner --log-level error --exit-code 9 ) >/dev/null 2>&1
  if [[ $? -ne 9 ]]; then
    _docs_lane_stamp "$stamps" secret-scan "$hash" could-not-run applicable "$gl_version" false
    DOCS_LANE_REASON="gitleaks canary not detected — the scan cannot be trusted"; rm -rf "$work"; return 1
  fi
  if [[ ! -x "$mdcli" ]]; then
    _docs_lane_stamp "$stamps" markdown-lint "$hash" could-not-run applicable "markdownlint-cli2/absent" false
    DOCS_LANE_REASON="markdownlint-cli2 not available in the candidate"; rm -rf "$work"; return 1
  fi
  local md_version; md_version="markdownlint-cli2/$( "$mdcli" --help 2>&1 | head -1 | tr -d '\n' )"
  ( cd "$work" && "$mdcli" --config "$work/mdlint.jsonc" canary.md ) >/dev/null 2>&1 && {
    _docs_lane_stamp "$stamps" markdown-lint "$hash" could-not-run applicable "$md_version" false
    DOCS_LANE_REASON="markdown lint canary not detected"; rm -rf "$work"; return 1; }
  ( cd "$work" && "$mdcli" --config "$work/mdlink.jsonc" canary.md ) >/dev/null 2>&1 && {
    _docs_lane_stamp "$stamps" link-check "$hash" could-not-run applicable "$md_version" false
    DOCS_LANE_REASON="link-check canary not detected"; rm -rf "$work"; return 1; }
  if [[ "$want_fm" -eq 1 ]]; then
    _docs_lane_frontmatter_check "$work/canary-frontmatter.md" >/dev/null 2>&1
    if [[ $? -ne 1 ]]; then
      _docs_lane_stamp "$stamps" frontmatter-schema "$hash" could-not-run applicable "$DOCS_LANE_VERSION" false
      DOCS_LANE_REASON="frontmatter schema canary not detected"; rm -rf "$work"; return 1
    fi
  fi

  _docs_lane_stamp "$stamps" path-classification "$hash" pass applicable "$DOCS_LANE_VERSION" true \
    ',"lane":"docs","changed_paths":'"$n"

  local p failed=0 scanned=0
  for p in "${paths[@]}"; do
    if [[ ! -f "$c/$p" ]]; then
      _docs_lane_stamp "$stamps" secret-scan "$hash" could-not-run applicable "$gl_version" true ',"detail":"'"$(jstr "changed path absent from the candidate: $p")"'"'
      DOCS_LANE_REASON="changed path absent from the candidate ($p)"; rm -rf "$work"; return 1
    fi
    scanned=$((scanned+1))
    ( cd "$work" && "$gitleaks" detect --no-git --source "$c/$p" --no-banner --log-level error --exit-code 9 ) >/dev/null 2>&1
    rc=$?
    if [[ $rc -eq 9 ]]; then
      _docs_lane_stamp "$stamps" secret-scan "$hash" fail applicable "$gl_version" true ',"detail":"'"$(jstr "secret pattern in $p")"'"'
      DOCS_LANE_REASON="secret pattern detected in $p"; rm -rf "$work"; return 2
    fi
    if [[ $rc -ne 0 ]]; then
      _docs_lane_stamp "$stamps" secret-scan "$hash" could-not-run applicable "$gl_version" true ',"detail":"'"$(jstr "gitleaks exited $rc on $p")"'"'
      DOCS_LANE_REASON="gitleaks exited $rc on $p"; rm -rf "$work"; return 2
    fi
  done
  _docs_lane_stamp "$stamps" secret-scan "$hash" pass applicable "$gl_version" true ',"scanned":'"$scanned"

  if ((${#mds[@]})); then
    out=$( cd "$c" && "$mdcli" --config "$work/mdlint.jsonc" "${mds[@]}" 2>&1 ); rc=$?
    if [[ $rc -ne 0 ]]; then
      _docs_lane_stamp "$stamps" markdown-lint "$hash" fail applicable "$md_version" true ',"detail":"'"$(jstr "$(printf '%s' "$out" | tail -c 400)")"'"'
      DOCS_LANE_REASON="markdown lint failed"; failed=1
    else
      _docs_lane_stamp "$stamps" markdown-lint "$hash" pass applicable "$md_version" true ',"scanned":'"${#mds[@]}"
    fi
    out=$( cd "$c" && "$mdcli" --config "$work/mdlink.jsonc" "${mds[@]}" 2>&1 ); rc=$?
    if [[ $rc -ne 0 ]]; then
      _docs_lane_stamp "$stamps" link-check "$hash" fail applicable "$md_version" true ',"detail":"'"$(jstr "$(printf '%s' "$out" | tail -c 400)")"'"'
      DOCS_LANE_REASON="link check failed"; failed=1
    else
      _docs_lane_stamp "$stamps" link-check "$hash" pass applicable "$md_version" true ',"scanned":'"${#mds[@]}"
    fi
  else
    _docs_lane_stamp "$stamps" markdown-lint "$hash" could-not-run not-applicable "$md_version" true "$discharge"
    _docs_lane_stamp "$stamps" link-check "$hash" could-not-run not-applicable "$md_version" true "$discharge"
  fi

  if ((${#prompts[@]})); then
    local -a fmargs=(); for p in "${prompts[@]}"; do fmargs+=("$c/$p"); done
    out=$(_docs_lane_frontmatter_check "${fmargs[@]}" 2>&1); rc=$?
    if [[ $rc -eq 1 ]]; then
      _docs_lane_stamp "$stamps" frontmatter-schema "$hash" fail applicable "$DOCS_LANE_VERSION" true ',"detail":"'"$(jstr "$(printf '%s' "$out" | tail -c 400)")"'"'
      DOCS_LANE_REASON="agent/skill frontmatter schema violation"; failed=1
    elif [[ $rc -ne 0 ]]; then
      _docs_lane_stamp "$stamps" frontmatter-schema "$hash" could-not-run applicable "$DOCS_LANE_VERSION" true ',"detail":"'"$(jstr "$(printf '%s' "$out" | tail -c 400)")"'"'
      DOCS_LANE_REASON="frontmatter schema check could not run"; failed=1
    else
      _docs_lane_stamp "$stamps" frontmatter-schema "$hash" pass applicable "$DOCS_LANE_VERSION" true ',"scanned":'"${#prompts[@]}"
    fi
  else
    _docs_lane_stamp "$stamps" frontmatter-schema "$hash" could-not-run not-applicable "$DOCS_LANE_VERSION" true "$discharge"
  fi

  _docs_lane_stamp "$stamps" typecheck "$hash" could-not-run not-applicable "$DOCS_LANE_VERSION" true "$discharge"
  _docs_lane_stamp "$stamps" test "$hash" could-not-run not-applicable "$DOCS_LANE_VERSION" true "$discharge"
  rm -rf "$work"
  [[ $failed -eq 0 ]] || return 2
  return 0
}

stage_a_enabled() {
  [[ "$(git -C "$1" config --get harness.landQueue 2>/dev/null || true)" == "stage-a" ]]
}

_stage_a_publisher_dir() {
  local common_dir
  common_dir=$(git -C "$1" rev-parse --path-format=absolute --git-common-dir 2>/dev/null) || return 3
  printf '%s/harness/publisher.git' "$common_dir"
}

_stage_a_candidate_dir() {
  local common_dir cand_base="${FINISH_BRANCH_CANDIDATE_DIR:-$HOME/.cache/finish-branch}"
  common_dir=$(git -C "$1" rev-parse --path-format=absolute --git-common-dir 2>/dev/null) || return 3
  printf '%s/candidate-%s' "$cand_base" "$(printf '%s' "$common_dir" | sha256sum | cut -c1-12)"
}

_stage_a_resolve_url() {
  local root=$1 origin_url=$2 origin_parent origin_name
  if [[ "$origin_url" != /* && "$origin_url" != *:* ]]; then
    origin_parent=$(cd "$root/$(dirname "$origin_url")" 2>/dev/null && pwd -P) || return 3
    origin_name=$(basename "$origin_url")
    origin_url="$origin_parent/$origin_name"
  fi
  printf '%s' "$origin_url"
}

_stage_a_origin_url() {
  local root=$1 access=${2:-fetch} origin_url
  if [[ "$access" == push ]]; then
    origin_url=$(git -C "$root" remote get-url --push origin 2>/dev/null) || return 3
  else
    origin_url=$(git -C "$root" remote get-url origin 2>/dev/null) || return 3
  fi
  _stage_a_resolve_url "$root" "$origin_url"
}

_stage_a_transport_key() {
  case "$1" in
    core.sshcommand|core.gitproxy|ssh.variant|http.*|credential.*|protocol.*|url.*|remote.origin.receivepack|remote.origin.uploadpack|remote.origin.proxy|remote.origin.proxyauthmethod|remote.origin.vcs|remote.origin.mirror|remote.origin.pushoption) return 0 ;;
    *) return 1 ;;
  esac
}

_stage_a_sync_transport_config() {
  local root=$1 target=$2 entry key value
  while IFS= read -r -d '' entry; do
    key=${entry%%$'\n'*}
    _stage_a_transport_key "$key" || continue
    git -C "$target" config --local --unset-all "$key" 2>/dev/null || true
  done < <(git -C "$target" config --local --null --list 2>/dev/null)
  while IFS= read -r -d '' entry; do
    key=${entry%%$'\n'*}; value=${entry#*$'\n'}
    _stage_a_transport_key "$key" || continue
    git -C "$target" config --local --add "$key" "$value" || return 3
  done < <(git -C "$root" config --local --null --list 2>/dev/null)
}

_stage_a_configure_candidate_origin() {
  local root=$1 candidate=$2 fetch_url push_url
  local push_urls=()
  fetch_url=$(_stage_a_origin_url "$root") || return 3
  mapfile -t push_urls < <(git -C "$root" remote get-url --push --all origin 2>/dev/null)
  ((${#push_urls[@]})) || return 3
  git -C "$candidate" remote add origin "$fetch_url" || return 3
  git -C "$candidate" config --unset-all remote.origin.pushurl 2>/dev/null || true
  for push_url in "${push_urls[@]}"; do
    push_url=$(_stage_a_resolve_url "$root" "$push_url") || return 3
    git -C "$candidate" config --add remote.origin.pushurl "$push_url" || return 3
  done
  _stage_a_sync_transport_config "$root" "$candidate"
}

_stage_a_remove_candidate() {
  rm -rf -- "$1" 2>/dev/null || true
}

_stage_a_remove_candidate_strict() {
  [[ ! -e "$1" && ! -L "$1" ]] || rm -rf -- "$1" || return 3
  [[ ! -e "$1" && ! -L "$1" ]]
}

_stage_a_sweep_bundles() {
  find "$1/harness" -maxdepth 1 -type f -name 'source-*.bundle' -delete
}

_stage_a_prepare_publisher() {
  local root=$1 publisher=$2 candidate=$3 origin_url
  origin_url=$(_stage_a_origin_url "$root") || return 3
  if [[ ! -d "$publisher" ]] \
    || [[ "$(git -C "$publisher" rev-parse --is-bare-repository 2>/dev/null || true)" != true ]] \
    || [[ -s "$publisher/objects/info/alternates" ]]; then
    _stage_a_remove_candidate "$candidate"
    rm -rf "$publisher" || return 3
    mkdir -p "$(dirname "$publisher")" || return 3
    git init --quiet --bare "$publisher" || return 3
  fi
  if git -C "$publisher" remote get-url origin >/dev/null 2>&1; then
    git -C "$publisher" remote set-url origin "$origin_url" || return 3
  else
    git -C "$publisher" remote add origin "$origin_url" || return 3
  fi
  _stage_a_sync_transport_config "$root" "$publisher" || return 3
  git -C "$publisher" config user.name "$(git -C "$root" config user.name 2>/dev/null || printf 'harness lander')"
  git -C "$publisher" config user.email "$(git -C "$root" config user.email 2>/dev/null || printf 'harness@localhost')"
}

_stage_a_install_candidate_guard() {
  local root=$1 candidate=$2 root_hook candidate_hooks candidate_hook guard_source
  root_hook=$(git -C "$root" rev-parse --path-format=absolute --git-path hooks/pre-push 2>/dev/null) || return 3
  candidate_hooks="$candidate/.git/harness/hooks"
  candidate_hook="$candidate_hooks/pre-push"
  guard_source="$(cd "$(dirname "${BASH_SOURCE[0]}")/../hooks" && pwd)/land-guard.pre-push"
  mkdir -p "$candidate_hooks" || return 3
  {
    printf '#!/usr/bin/env bash\nset -euo pipefail\n'
    printf 'input=$(mktemp "${TMPDIR:-/tmp}/harness-pre-push.XXXXXX")\n'
    printf 'trap '\''rm -f "$input"'\'' EXIT\ncat > "$input"\n'
    if [[ -x "$root_hook" ]]; then
      printf '%q "$@" < "$input"\n' "$root_hook"
    fi
    printf 'bash %q "$@" < "$input"\n' "$guard_source"
  } > "$candidate_hook" || return 3
  chmod 0755 "$candidate_hook" || return 3
  git -C "$candidate" config core.hooksPath "$candidate_hooks" || return 3
}

_stage_a_fetch_main() {
  git -C "$1" fetch --quiet --no-tags --force origin refs/heads/main:refs/remotes/origin/main
}

_stage_a_refresh_publisher() {
  local root=$1 publisher=$2 candidate=$3
  _stage_a_prepare_publisher "$root" "$publisher" "$candidate" || return 3
  _stage_a_fetch_main "$publisher" && return 0
  _stage_a_remove_candidate "$candidate"
  rm -rf "$publisher" || return 3
  _stage_a_prepare_publisher "$root" "$publisher" "$candidate" || return 3
  _stage_a_fetch_main "$publisher"
}

_stage_a_remote_contains() {
  local root=$1 sha=$2 publisher candidate
  publisher=$(_stage_a_publisher_dir "$root") || return 3
  candidate=$(_stage_a_candidate_dir "$root") || return 3
  _stage_a_refresh_publisher "$root" "$publisher" "$candidate" || return 1
  git -C "$publisher" merge-base --is-ancestor "$sha" refs/remotes/origin/main 2>/dev/null
}

_stage_a_candidate_guard() {
  local candidate=$1
  local marked; marked=$(conflict_marked "$candidate")
  if [[ -n "$marked" ]]; then
    local files; files=$(printf '%s' "$marked" | awk 'BEGIN{printf "["}{if(n++)printf ",";printf "\"%s\"",$0}END{printf "]"}')
    emit_status candidate-conflict-markers '"files":'"$files"; return 20
  fi
  local dirty; dirty=$(dirty_tracked_json "$candidate")
  if [[ "$dirty" != "[]" ]]; then
    emit_status candidate-dirty-tree '"files":'"$dirty"; return 20
  fi
  return 0
}

_stage_a_source_guard() {
  local wt=$1
  local marked; marked=$(conflict_marked "$wt")
  if [[ -n "$marked" ]]; then
    local files; files=$(printf '%s' "$marked" | awk 'BEGIN{printf "["}{if(n++)printf ",";printf "\"%s\"",$0}END{printf "]"}')
    emit_status source-conflict-markers '"files":'"$files"; return 20
  fi
  local dirty; dirty=$(dirty_tracked_json "$wt")
  if [[ "$dirty" != "[]" ]]; then
    emit_status source-dirty-tree '"files":'"$dirty"; return 20
  fi
  return 0
}

_stage_a_cleanup() {
  local root=$1 branch=$2 wt=$3 source_sha=$4 assets_ok=$5
  if [[ -z "$assets_ok" && -d "$wt" ]]; then
    local found=()
    local p; for p in tmp .cache .turbo dist coverage; do
      [[ -d "$wt/$p" ]] && [[ -n "$(ls -A "$wt/$p" 2>/dev/null)" ]] && found+=("$p")
    done
    if ((${#found[@]})); then
      local fj; fj=$(printf '%s\n' "${found[@]}" | awk 'BEGIN{printf "["}{if(n++)printf ",";printf "\"%s\"",$0}END{printf "]"}')
      emit_status assets-present '"paths":'"$fj"',"recovery":"these gitignored dirs exist ONLY in the worktree and removal is irreversible — copy anything worth keeping to <main>/tmp, then re-run cleanup with --assets-ok"'
      return 20
    fi
  fi

  local current; current=$(git -C "$root" rev-parse -q --verify "refs/heads/$branch^{commit}" 2>/dev/null) || {
    emit_status source-moved '"branch_kept":true,"detail":"source branch no longer resolves; cleanup retained the worktree"'; return 0
  }
  if [[ "$current" != "$source_sha" ]]; then
    emit_status source-moved '"branch_kept":true,"detail":"source branch moved after snapshot; cleanup retained branch and worktree"'; return 0
  fi
  if [[ -d "$wt" ]]; then
    git -C "$root" worktree remove "$wt" --force >/dev/null 2>&1 || {
      emit_status worktree-remove-failed '"detail":"git worktree remove --force failed for '"$(jstr "$wt")"'"'; return 20
    }
  fi
  current=$(git -C "$root" rev-parse -q --verify "refs/heads/$branch^{commit}" 2>/dev/null) || {
    [[ ! -d "$wt" ]] && { emit_status cleaned '"branch_kept":false'; return 0; }
    emit_status branch-delete-failed '"detail":"source branch disappeared before deletion"'; return 20
  }
  if [[ "$current" != "$source_sha" ]]; then
    emit_status source-moved '"branch_kept":true,"detail":"source branch moved during cleanup; retained the moved branch"'; return 0
  fi
  git -C "$root" branch -D "$branch" >/dev/null 2>&1 || {
    emit_status branch-delete-failed '"detail":"git branch -D '"$(jstr "$branch")"' failed after reachability proof"'; return 20
  }
  if [[ "$branch" == plan/* ]]; then
    local _rp="${RUN_PLAN_RUN_DIR:-$HOME/.claude/workflows/.run}" _slug=${branch#plan/}
    rm -f "$_rp/$_slug.owner" "$_rp/$_slug.live" "$_rp/$_slug.pid" 2>/dev/null || true
  fi
  emit_status cleaned '"branch_kept":false'
}

_stage_a_land_core() (
  local root=$1 branch=$2 source_sha=$3 rescue_ref=$4 testcmd=$5 depcmd=$6 verified_tree=${7:-}
  need_repo "$root" || return 3
  local common_dir publisher candidate authority_file="" source_bundle="" source_ref="refs/harness/source" push_err="" guard rc
  common_dir=$(git -C "$root" rev-parse --path-format=absolute --git-common-dir 2>/dev/null) || return 3
  publisher=$(_stage_a_publisher_dir "$root") || return 3
  candidate=$(_stage_a_candidate_dir "$root") || return 3
  local lock_timeout=${FINISH_BRANCH_LAND_LOCK_TIMEOUT_SECONDS:-300}
  [[ "$lock_timeout" =~ ^[1-9][0-9]*$ ]] || {
    emit_status bad-land-lock-timeout '"detail":"FINISH_BRANCH_LAND_LOCK_TIMEOUT_SECONDS must be a positive integer"'; return 3
  }
  mkdir -p "$common_dir/harness" "$(dirname "$candidate")" || return 3
  local lock_fd; exec {lock_fd}>"$common_dir/harness/land.lock" || return 3
  if ! flock -w "$lock_timeout" "$lock_fd"; then
    emit_status land-lock-timeout '"detail":"timed out waiting for the land lock the pre-push guard requires"'; return 20
  fi

  _stage_a_release() {
    [[ -n "$authority_file" ]] && rm -f "$authority_file" 2>/dev/null || true
    _stage_a_remove_candidate "$candidate"
    [[ -n "$source_bundle" ]] && rm -f "$source_bundle" 2>/dev/null || true
  }
  trap _stage_a_release EXIT
  trap 'exit 20' HUP INT TERM

  _stage_a_sweep_bundles "$common_dir" || return 3
  if ! _stage_a_refresh_publisher "$root" "$publisher" "$candidate"; then
    emit_status fetch-failed '"detail":"isolated publisher could not fetch origin main"'; return 20
  fi
  source_bundle=$(mktemp "$common_dir/harness/source-XXXXXX.bundle") || return 3
  if ! git -C "$root" bundle create "$source_bundle" "$rescue_ref" >/dev/null 2>&1; then
    emit_status source-export-failed '"detail":"ticket source object is unavailable; unrelated tickets remain serviceable"'; return 20
  fi
  [[ "$(git bundle list-heads "$source_bundle" "$rescue_ref" 2>/dev/null | awk 'NR == 1 {print $1}')" == "$source_sha" ]] || {
    emit_status source-export-failed '"detail":"exported ticket source identity differs from queued head"'; return 20
  }

  local attempt=1 base_sha latest_sha candidate_sha candidate_tree merge_log tlog detail nonce
  while (( attempt <= 3 )); do
    if ! _stage_a_fetch_main "$publisher"; then
      emit_status fetch-failed '"detail":"isolated publisher fetch origin main failed"'; return 20
    fi
    base_sha=$(git -C "$publisher" rev-parse refs/remotes/origin/main 2>/dev/null) || return 3
    git -C "$publisher" update-ref refs/heads/main "$base_sha" || return 3
    _stage_a_remove_candidate "$candidate"
    if ! git init --quiet "$candidate"; then
      emit_status candidate-create-failed '"detail":"self-contained candidate repository creation failed"'; return 20
    fi
    git -C "$candidate" config user.name "$(git -C "$publisher" config user.name)" || return 3
    git -C "$candidate" config user.email "$(git -C "$publisher" config user.email)" || return 3
    _stage_a_configure_candidate_origin "$root" "$candidate" || return 3
    if ! git -C "$candidate" fetch --quiet --no-tags "$publisher" \
      "+refs/remotes/origin/main:refs/remotes/origin/main" \
      || ! git -C "$candidate" fetch --quiet --no-tags "$source_bundle" \
      "+$rescue_ref:$source_ref"; then
      emit_status source-import-failed '"detail":"candidate could not import isolated base and ticket source"'; return 20
    fi
    [[ "$(git -C "$candidate" rev-parse -q --verify "$source_ref^{commit}" 2>/dev/null || true)" == "$source_sha" ]] || {
      emit_status source-import-failed '"detail":"imported ticket source identity differs from queued head"'; return 20
    }
    _stage_a_install_candidate_guard "$root" "$candidate" || return 3
    git -C "$candidate" checkout --quiet --detach "$base_sha" || return 3
    merge_log="$candidate/.git/harness-merge.log"
    if ! git -C "$candidate" merge --no-ff -m "harness: land $branch at $source_sha" "$source_ref" >"$merge_log" 2>&1; then
      if git -C "$candidate" rev-parse -q --verify MERGE_HEAD >/dev/null 2>&1; then
        emit_status candidate-conflict '"attempt":'"$attempt"',"rescue_ref":"'"$(jstr "$rescue_ref")"'"'; return 20
      fi
      emit_status candidate-merge-failed '"attempt":'"$attempt"',"detail":"candidate merge failed without a conflict"'; return 20
    fi

    if [[ -n "$depcmd" ]]; then
      if ! ( cd "$candidate" && bash -c "$depcmd" ) >"$candidate/.git/harness-deps.log" 2>&1; then
        emit_status candidate-deps-failed '"attempt":'"$attempt"; return 20
      fi
    fi
    guard=$(_stage_a_candidate_guard "$candidate"); rc=$?
    [[ $rc -eq 0 ]] || { printf '%s\n' "$guard"; return "$rc"; }

    local merged_sha lane_paths stamps=$common_dir/harness/land-stamps.jsonl lane=full
    merged_sha=$(git -C "$candidate" rev-parse HEAD 2>/dev/null) || return 3
    : > "$stamps" 2>/dev/null || true
    lane_paths="$candidate/.git/docs-lane-paths"
    if _docs_lane_classifier_canary; then
      if _docs_lane_classify "$candidate" "$base_sha" "$merged_sha" "$lane_paths"; then lane=docs; fi
    else
      DOCS_LANE_REASON="path classifier canary failed"
    fi
    if [[ "$lane" == docs ]]; then
      echo "finish-branch: docs-only path set — docs lane (attempt $attempt)" >&2
      _docs_lane_gate "$candidate" "$stamps" "$DOCS_LANE_HASH" "$lane_paths" "$DOCS_LANE_FRONTMATTER"; rc=$?
      if [[ $rc -eq 2 ]]; then
        emit_status candidate-docs-lane-failed '"attempt":'"$attempt"',"detail":"'"$(jstr "$DOCS_LANE_REASON")"'","stamps":"'"$(jstr "$stamps")"'"'; return 20
      fi
      if [[ $rc -ne 0 ]]; then
        lane=full
        echo "finish-branch: docs lane unavailable ($DOCS_LANE_REASON) — full gate" >&2
      fi
    fi
    if [[ "$lane" != docs ]]; then
      _docs_lane_stamp "$stamps" path-classification "$DOCS_LANE_HASH" pass applicable "$DOCS_LANE_VERSION" true \
        ',"lane":"full","detail":"'"$(jstr "${DOCS_LANE_REASON:-not a docs-only path set}")"'"'
      echo "finish-branch: candidate typecheck gate running (attempt $attempt)" >&2
      tlog=$(_typecheck_touched_packages "$candidate" "$base_sha" "$merged_sha" 2>&1); rc=$?
      if [[ $rc -ne 0 ]]; then
        detail="candidate typecheck exited $rc — do NOT land"
        [[ $rc -eq 124 || $rc -eq 137 ]] && detail="candidate typecheck timed out after ${FINISH_BRANCH_TYPECHECK_TIMEOUT_SECONDS:-600}s — do NOT land"
        _docs_lane_stamp "$stamps" typecheck "$DOCS_LANE_HASH" fail applicable "$DOCS_LANE_VERSION" true \
          ',"detail":"'"$(jstr "$detail")"'"'
        emit_status candidate-typecheck-failed '"attempt":'"$attempt"',"detail":"'"$(jstr "$detail")"'","tail":"'"$(jstr "$(printf '%s' "$tlog" | tail -c 400)")"'"'; return 20
      fi
      _docs_lane_stamp "$stamps" typecheck "$DOCS_LANE_HASH" pass applicable "$DOCS_LANE_VERSION" true
      echo "finish-branch: candidate test gate running (attempt $attempt, timeout ${FINISH_BRANCH_TEST_TIMEOUT_SECONDS:-1800}s)" >&2
      tlog=$(run_test_gate "$candidate" "$testcmd" 2>&1); rc=$?
      if [[ $rc -ne 0 ]]; then
        detail="candidate test command exited $rc — do NOT land"
        [[ $rc -eq 124 || $rc -eq 137 ]] && detail="candidate test command timed out after ${FINISH_BRANCH_TEST_TIMEOUT_SECONDS:-1800}s — do NOT land"
        _docs_lane_stamp "$stamps" test "$DOCS_LANE_HASH" fail applicable "$DOCS_LANE_VERSION" true
        emit_status candidate-tests-failed '"attempt":'"$attempt"',"detail":"'"$(jstr "$detail")"'","tail":"'"$(jstr "$(printf '%s' "$tlog" | tail -c 400)")"'"'; return 20
      fi
      _docs_lane_stamp "$stamps" test "$DOCS_LANE_HASH" pass applicable "$DOCS_LANE_VERSION" true
    fi
    rm -f "$lane_paths" 2>/dev/null || true
    guard=$(_stage_a_candidate_guard "$candidate"); rc=$?
    [[ $rc -eq 0 ]] || { printf '%s\n' "$guard"; return "$rc"; }

    if ! _stage_a_fetch_main "$publisher"; then
      emit_status fetch-failed '"detail":"post-gate isolated publisher fetch origin main failed"'; return 20
    fi
    latest_sha=$(git -C "$publisher" rev-parse refs/remotes/origin/main 2>/dev/null) || return 3
    if [[ "$latest_sha" != "$base_sha" ]]; then
      _stage_a_remove_candidate "$candidate"
      if (( attempt == 3 )); then
        emit_status remote-churn '"attempts":3'; return 20
      fi
      attempt=$((attempt+1)); continue
    fi

    candidate_sha=$(git -C "$candidate" rev-parse HEAD 2>/dev/null) || return 3
    candidate_tree=$(git -C "$candidate" rev-parse "$candidate_sha^{tree}" 2>/dev/null) || return 3
    if [[ -n "$verified_tree" && "$candidate_tree" != "$verified_tree" ]]; then
      emit_status e2e-stale-base '"detail":"candidate tree differs from e2e-verified source tree"'; return 20
    fi

    IFS= read -r nonce < /proc/sys/kernel/random/uuid || {
      echo "finish-branch: could not generate land authority nonce" >&2; return 3
    }
    [[ -n "$nonce" ]] || { echo "finish-branch: empty land authority nonce" >&2; return 3; }
    authority_file="$candidate/.git/harness/land-authority.$nonce"
    mkdir -p "$(dirname "$authority_file")" || return 3
    ( umask 077; set -o noclobber; printf '%s\n' "$common_dir/harness/landq/conductor.lock" > "$authority_file" ) || return 3
    if push_err=$(HARNESS_LAND_TOKEN="$nonce" git -C "$candidate" push origin "HEAD:refs/heads/main" 2>&1 >/dev/null); then
      rm -f "$authority_file" || { emit_status authority-cleanup-failed '"detail":"could not remove land authority file"'; return 20; }
      authority_file=""
    else
      rm -f "$authority_file" || { emit_status authority-cleanup-failed '"detail":"could not remove land authority file"'; return 20; }
      authority_file=""
      if _stage_a_fetch_main "$publisher"; then
        latest_sha=$(git -C "$publisher" rev-parse refs/remotes/origin/main 2>/dev/null) || return 3
        if [[ "$latest_sha" != "$base_sha" ]]; then
          _stage_a_remove_candidate "$candidate"
          if (( attempt == 3 )); then emit_status remote-churn '"attempts":3'; return 20; fi
          attempt=$((attempt+1)); continue
        fi
      fi
      emit_status push-failed '"detail":"isolated candidate push to origin main failed","stderr":"'"$(jstr "$(printf '%s' "$push_err" | tail -c 600)")"'"'; return 20
    fi

    if ! _stage_a_fetch_main "$publisher"; then
      emit_status reachability-failed '"detail":"isolated publisher could not fetch origin main for reachability proof"'; return 20
    fi
    latest_sha=$(git -C "$publisher" rev-parse refs/remotes/origin/main 2>/dev/null) || return 3
    if [[ "$latest_sha" != "$candidate_sha" ]]; then
      emit_status reachability-failed '"detail":"origin main is not at verified candidate after push — refusing destructive cleanup"'; return 20
    fi
    emit_status pushed '"candidate_sha":"'"$candidate_sha"'"'; return 0
  done
)

# ── land queue: FIFO tickets, one conductor, no timeout ──────────────────────────────────────
# A landing agent enqueues a ticket and then either finds a verdict, waits behind the process
# holding the conductor lock, or takes that lock and serves the queue itself. A ticket is the
# complete job description; a conductor resolves nothing from its own environment.

_landq_enc() { printf '%s' "$1" | base64 -w0; }
_landq_dec() { printf '%s' "$1" | base64 -d; }

_landq_dir() {
  local common_dir
  common_dir=$(git -C "$1" rev-parse --path-format=absolute --git-common-dir 2>/dev/null) || return 3
  printf '%s/harness/landq' "$common_dir"
}

_landq_ticket_id() {
  local u; IFS= read -r u < /proc/sys/kernel/random/uuid || return 3
  [[ -n "$u" ]] || return 3
  printf 'ticket.%s' "${u//-/}"
}

# Prune tickets whose owner lock is acquirable (owner gone: nobody is waiting for that verdict and
# nobody would run its owner-side cleanup, so it is dropped, never landed), then print the survivors
# in FIFO order.
_landq_live() {
  local dir=$1 qfd ofd t live=()
  exec {qfd}>"$dir/queue.lock" || return 3
  flock "$qfd" || return 3
  if [[ -f "$dir/queue" ]]; then
    while read -r t; do
      [[ "$t" =~ ^ticket\.[0-9a-f]{32}$ ]] || continue
      if ! exec {ofd}>"$dir/$t.lock"; then flock -u "$qfd"; return 3; fi
      if flock -n "$ofd"; then
        exec {ofd}>&-
        rm -f "$dir/$t.lock" "$dir/$t.job" "$dir/$t.verdict"
      else
        live+=("$t"); exec {ofd}>&-
      fi
    done < "$dir/queue"
  fi
  if ((${#live[@]})); then
    printf '%s\n' "${live[@]}" > "$dir/queue.new" && mv "$dir/queue.new" "$dir/queue"
  else
    : > "$dir/queue"
  fi
  flock -u "$qfd"; exec {qfd}>&-
  ((${#live[@]})) && printf '%s\n' "${live[@]}"
  return 0
}

# Drop a collected ticket from the queue and remove its files under the same lock a lister takes,
# so no lister can see the entry after the files are gone.
_landq_release() {
  local dir=$1 t=$2 qfd
  exec {qfd}>"$dir/queue.lock" || return 3
  flock "$qfd" || return 3
  if [[ -f "$dir/queue" ]]; then
    grep -Fxv -- "$t" "$dir/queue" > "$dir/queue.new" || true
    mv "$dir/queue.new" "$dir/queue"
  fi
  rm -f "$dir/$t.job" "$dir/$t.verdict" "$dir/$t.lock"
  flock -u "$qfd"; exec {qfd}>&-
}

_landq_verdict_write() {
  local dir=$1 t=$2 rc=$3 out=$4 tmp
  tmp="$dir/$t.verdict.$$"
  { printf '%s\n' "$rc"; [[ -n "$out" ]] && printf '%s\n' "$out"; } > "$tmp" || return 3
  mv "$tmp" "$dir/$t.verdict"
}

_landq_reject() {
  _landq_verdict_write "$1" "$2" 20 "$(emit_status ticket-rejected '"detail":"'"$(jstr "$3")"'"')"
}

# Gate + land one ticket, ALWAYS leaving a verdict behind.
_landq_serve() {
  local dir=$1 t=$2 job
  job="$dir/$t.job"
  [[ -f "$job" ]] || { _landq_reject "$dir" "$t" "job description missing"; return 0; }
  local k v line
  local J_root="" J_wt="" J_branch="" J_head="" J_base="" J_rescue_ref="" J_mode="" \
        J_testcmd="" J_depcmd="" J_assets_ok="" J_verified_tree="" J_gate_class="" \
        J_owner_pid="" J_enqueued_at="" J_queue_depth=""
  while IFS= read -r line; do
    k=${line%% *}; v=${line#* }
    [[ "$k" != "$line" ]] || { _landq_reject "$dir" "$t" "malformed job line"; return 0; }
    case "$k" in
      root|wt|branch|head|base|rescue_ref|mode|testcmd|depcmd|assets_ok|verified_tree|gate_class|owner_pid|enqueued_at|queue_depth) ;;
      *) _landq_reject "$dir" "$t" "unknown job field: $k"; return 0;;
    esac
    v=$(_landq_dec "$v") || { _landq_reject "$dir" "$t" "undecodable value for field: $k"; return 0; }
    printf -v "J_$k" '%s' "$v"
  done < "$job"
  local req vn
  for req in root wt branch head base rescue_ref mode testcmd gate_class; do
    vn="J_$req"
    [[ -n "${!vn}" ]] || { _landq_reject "$dir" "$t" "missing job field: $req"; return 0; }
  done
  [[ "$J_mode" == stage-a ]] || { _landq_reject "$dir" "$t" "unsupported job mode: $J_mode"; return 0; }
  [[ "$J_base" == refs/remotes/origin/main ]] || { _landq_reject "$dir" "$t" "unsupported job base: $J_base"; return 0; }

  # A conductor that pushed and then died leaves a landed head and no verdict: git, not a journal,
  # is what says so.
  if _stage_a_remote_contains "$J_root" "$J_head"; then
    local recovered_candidate recovered_common
    recovered_candidate=$(_stage_a_candidate_dir "$J_root") || {
      _landq_reject "$dir" "$t" "could not resolve stale candidate path during recovery"; return 0
    }
    recovered_common=$(git -C "$J_root" rev-parse --path-format=absolute --git-common-dir 2>/dev/null) || {
      _landq_reject "$dir" "$t" "could not resolve shared storage during recovery"; return 0
    }
    _stage_a_remove_candidate_strict "$recovered_candidate" || {
      _landq_reject "$dir" "$t" "could not remove stale authority-bearing candidate"; return 0
    }
    _stage_a_sweep_bundles "$recovered_common" || {
      _landq_reject "$dir" "$t" "could not remove stale ticket bundles"; return 0
    }
    _landq_verdict_write "$dir" "$t" 0 "$(emit_status pushed '"recovered":true')"
    return 0
  fi

  local out rc
  out=$(_stage_a_land_core "$J_root" "$J_branch" "$J_head" "$J_rescue_ref" "$J_testcmd" "$J_depcmd" "$J_verified_tree"); rc=$?
  _landq_verdict_write "$dir" "$t" "$rc" "$out"
}

# Serve every live ticket without a verdict, FIFO, one at a time, until a pass serves none.
# The listing is captured, not piped: inside a process substitution a failing lister is
# indistinguishable from an empty queue, and "nothing to serve" is the success exit.
# Exit 4 means the queue could not be listed, which is not the same failure as an unserved ticket.
_landq_conduct() {
  local dir=$1 t served listing
  while :; do
    served=0
    listing=$(_landq_live "$dir") || return 4
    while read -r t; do
      [[ -n "$t" ]] || continue
      [[ -f "$dir/$t.verdict" ]] && continue
      _landq_serve "$dir" "$t" || return 3
      [[ -f "$dir/$t.verdict" ]] || return 3
      served=1
    done <<< "$listing"
    (( served )) || return 0
  done
}

# Advisory at enqueue (train composition); the candidate-side classification inside the gate is the
# authoritative one. Same classifier, called from both sites.
_landq_gate_class() {
  local root=$1 head=$2 base tmp cls=full
  base=$(git -C "$root" merge-base refs/remotes/origin/main "$head" 2>/dev/null) || { printf full; return 0; }
  tmp=$(mktemp) || { printf full; return 0; }
  if _docs_lane_classifier_canary && _docs_lane_classify "$root" "$base" "$head" "$tmp"; then cls=docs; fi
  rm -f "$tmp"
  printf '%s' "$cls"
}

_stage_a_land() (
  local root=$1 wt=$2 branch=$3 testcmd=$4 depcmd=$5 assets_ok=$6 verified_tree=${7:-}
  need_repo "$root" || return 3; need_repo "$wt" || return 3
  command -v flock >/dev/null 2>&1 || { echo "finish-branch: flock is required for stage-a landing" >&2; return 3; }
  command -v base64 >/dev/null 2>&1 || { echo "finish-branch: base64 is required for stage-a landing" >&2; return 3; }

  local source_sha safe timestamp rescue_ref guard rc
  source_sha=$(git -C "$root" rev-parse -q --verify "refs/heads/$branch^{commit}" 2>/dev/null) || {
    echo "finish-branch: source branch does not resolve: $branch" >&2; return 3
  }
  guard=$(_stage_a_source_guard "$wt"); rc=$?
  [[ $rc -eq 0 ]] || { printf '%s\n' "$guard"; return "$rc"; }
  safe=${branch//[^[:alnum:]._-]/-}
  timestamp=$(date -u +%Y%m%dT%H%M%S%NZ) || return 3
  rescue_ref="refs/rescue/land/$timestamp-$safe"
  git -C "$root" update-ref "$rescue_ref" "$source_sha" || return 3

  local qdir ticket
  qdir=$(_landq_dir "$root") || return 3
  mkdir -p "$qdir" || return 3
  ticket=$(_landq_ticket_id) || return 3

  # Own the ticket BEFORE it is visible in the queue, or a conductor can prune it as ownerless.
  local tfd; exec {tfd}>"$qdir/$ticket.lock" || return 3
  flock -n "$tfd" || return 3

  local gate_class depth qfd
  gate_class=$(_landq_gate_class "$root" "$source_sha")
  {
    printf 'root %s\n'          "$(_landq_enc "$root")"
    printf 'wt %s\n'            "$(_landq_enc "$wt")"
    printf 'branch %s\n'        "$(_landq_enc "$branch")"
    printf 'head %s\n'          "$(_landq_enc "$source_sha")"
    printf 'base %s\n'          "$(_landq_enc refs/remotes/origin/main)"
    printf 'rescue_ref %s\n'    "$(_landq_enc "$rescue_ref")"
    printf 'mode %s\n'          "$(_landq_enc stage-a)"
    printf 'testcmd %s\n'       "$(_landq_enc "$testcmd")"
    printf 'depcmd %s\n'        "$(_landq_enc "$depcmd")"
    printf 'assets_ok %s\n'     "$(_landq_enc "$assets_ok")"
    printf 'verified_tree %s\n' "$(_landq_enc "$verified_tree")"
    printf 'gate_class %s\n'    "$(_landq_enc "$gate_class")"
    printf 'owner_pid %s\n'     "$(_landq_enc "$$")"
    printf 'enqueued_at %s\n'   "$(_landq_enc "$(date -u +%Y-%m-%dT%H:%M:%SZ)")"
  } > "$qdir/$ticket.job.tmp" || return 3
  mv "$qdir/$ticket.job.tmp" "$qdir/$ticket.job" || return 3

  # Prune first: depth must count tickets that are actually waiting, not ownerless leftovers.
  _landq_live "$qdir" >/dev/null || return 3
  exec {qfd}>"$qdir/queue.lock" || return 3
  flock "$qfd" || return 3
  depth=$(awk 'END{print NR}' "$qdir/queue" 2>/dev/null); [[ -n "$depth" ]] || depth=0
  # An unchecked append leaves a ticket nobody will ever serve, and the owner waits for a
  # verdict that cannot arrive.
  if ! printf '%s %s\n' "queue_depth" "$(_landq_enc "$depth")" >> "$qdir/$ticket.job" \
    || ! printf '%s\n' "$ticket" >> "$qdir/queue"; then
    flock -u "$qfd"; exec {qfd}>&-; return 3
  fi
  flock -u "$qfd"; exec {qfd}>&-
  printf '{"ticket":"%s","branch":"%s","gate_class":"%s","queue_depth_at_arrival":%s,"at":"%s"}\n' \
    "$ticket" "$(jstr "$branch")" "$gate_class" "$depth" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> "$qdir/log"
  echo "finish-branch: land queue — ticket $ticket, gate class $gate_class, queue depth at arrival $depth" >&2

  local cfd waited=0
  exec {cfd}>"$qdir/conductor.lock" || return 3
  while [[ ! -f "$qdir/$ticket.verdict" ]]; do
    if flock -n "$cfd"; then
      echo "finish-branch: land queue — conducting" >&2
      _landq_conduct "$qdir"; rc=$?
      flock -u "$cfd"
      # Abandoning the ticket here without releasing it leaves an entry no owner will ever
      # collect, inflating the depth every later lander queues behind.
      if [[ $rc -ne 0 ]]; then
        _landq_release "$qdir" "$ticket"
        if [[ $rc -eq 4 ]]; then
          emit_status land-queue-failed '"detail":"conductor could not list the land queue"'; return 20
        fi
        emit_status land-queue-failed '"detail":"conductor could not produce a verdict for every live ticket"'; return 20
      fi
      if [[ ! -f "$qdir/$ticket.verdict" ]]; then
        _landq_release "$qdir" "$ticket"
        emit_status land-queue-failed '"detail":"conducted the queue without a verdict for this ticket"'; return 20
      fi
      break
    fi
    sleep 2; waited=$((waited+2))
    (( waited == 2 || waited % 30 == 0 )) && echo "finish-branch: land queue — waiting for the conductor (${waited}s, ticket $ticket)" >&2
  done

  local vrc vout vstatus
  IFS= read -r vrc < "$qdir/$ticket.verdict" || return 3
  vout=$(tail -n +2 "$qdir/$ticket.verdict")
  _landq_release "$qdir" "$ticket"
  [[ "$vrc" =~ ^[0-9]+$ ]] || { echo "finish-branch: malformed land verdict for $ticket" >&2; return 3; }
  vstatus=$(jq_status "$vout")
  if (( vrc != 0 )); then printf '%s\n' "$vout"; return "$vrc"; fi
  if [[ "$vstatus" != "pushed" ]]; then
    printf '%s\n' "$vout"
    emit_status land-queue-failed '"detail":"verdict claims success with unexpected status '"$(jstr "$vstatus")"'"'; return 20
  fi

  # The conductor drains behind us, so origin/main may already have moved past our head: ancestry,
  # not equality, is what proves this branch landed before anything destructive runs.
  flock "$cfd" || { emit_status reachability-failed '"detail":"could not lock isolated publisher for owner-side reachability proof"'; return 20; }
  _stage_a_remote_contains "$root" "$source_sha"; local contains_rc=$?
  flock -u "$cfd"
  if [[ $contains_rc -ne 0 ]]; then
    emit_status reachability-failed '"detail":"landed head is not an ancestor of origin main in isolated publisher — refusing destructive cleanup"'; return 20
  fi
  # The queue wait widens the window between the pre-enqueue cleanliness proof and the forced
  # worktree removal, so the tree is re-proven clean immediately before anything is destroyed.
  guard=$(_stage_a_source_guard "$wt"); rc=$?
  [[ $rc -eq 0 ]] || { printf '%s\n' "$guard"; return "$rc"; }
  _stage_a_cleanup "$root" "$branch" "$wt" "$source_sha" "$assets_ok"; return $?
)

_stage_a_promote() {
  local root=$1 wt=$2 branch=$3 testcmd=$4 depcmd=$5 assets_ok=$6 verified_tree=${7:-}
  local result rc status
  result=$(_stage_a_land "$root" "$wt" "$branch" "$testcmd" "$depcmd" "$assets_ok" "$verified_tree"); rc=$?
  if [[ $rc -eq 3 ]]; then echo "$result" >&2; return 3; fi
  status=$(jq_status "$result")
  if [[ $rc -eq 0 && "$status" == "cleaned" ]]; then
    emit_ladder landed done none "candidate merged to origin/main, source worktree removed, branch deleted"; return 0
  fi
  if [[ $rc -eq 0 && "$status" == "source-moved" ]]; then
    emit_ladder landed done none "candidate merged to origin/main; moved source branch and worktree retained"; return 0
  fi
  printf '%s\n' "$result"
  emit_ladder candidate-land "$status" fix-then-rerun "serialized candidate land stopped — source and rescue ref retained"
  return 20
}

# ── drift: validate a project's FROZEN land facts still hold (fail-closed) ────────────────────
# Args: --root R --base B --mode M [--anchor KIND:VAL]. Emits {"status":"ok"} or, on divergence,
# the ladder fault line and returns 3. The single source of truth for "are this project's frozen
# facts still valid" — the per-project wrapper holds only the DATA, never this logic.
#   anchor KIND:VAL — deploy:<relpath-from-root>  (file must exist; merge-to-main deploy-on-push)
#                     remote:<url>                (origin remote-url must equal; PR destination)
#                     (empty/absent)              → no anchor check
drift() {
  local root="" base="" mode="" anchor=""
  while [[ $# -gt 0 ]]; do case "$1" in
    --root) root=$2; shift 2;; --base) base=$2; shift 2;;
    --mode) mode=$2; shift 2;; --anchor) anchor=$2; shift 2;;
    *) echo "finish-branch drift: unknown arg: $1" >&2; return 3;; esac; done
  [[ -n "$root" && -n "$base" && -n "$mode" ]] || { echo "usage: finish-branch.sh drift --root R --base B --mode M [--anchor KIND:VAL]" >&2; return 3; }

  [[ -e "$root/.git" ]] || { emit_ladder drift fault reinit "PROJECT_ROOT is not a git repo: $root"; return 3; }
  git -C "$root" rev-parse -q --verify "$base^{commit}" >/dev/null 2>&1 \
    || { emit_ladder drift fault reinit "base '$base' no longer resolves — re-init"; return 3; }
  [[ "$mode" == "merge-to-main" || "$mode" == "pr" || "$mode" == "deploy-verify" ]] \
    || { emit_ladder drift fault reinit "unknown LAND_MODE '$mode'"; return 3; }
  if [[ -n "$anchor" ]]; then
    local kind=${anchor%%:*} val=${anchor#*:}
    case "$kind" in
      deploy)
        [[ -f "$root/$val" ]] || { emit_ladder drift fault reinit "deploy signal gone ($val) — deploy mechanism changed, re-init"; return 3; };;
      remote)
        local actual; actual=$(git -C "$root" remote get-url origin 2>/dev/null)
        [[ "$actual" == "$val" ]] || { emit_ladder drift fault reinit "origin remote moved ('$actual' != frozen '$val') — PR destination changed, re-init"; return 3; };;
      *) emit_ladder drift fault reinit "unknown anchor kind '$kind'"; return 3;;
    esac
  fi
  _drift_land_guard "$root" || return 3
  return 0
}

# A pre-push guard from a different generation than this lander rejects its own authority, so a
# guarded repo whose installed marker block differs from the shipped one is drift, not a warning.
_drift_land_guard() {
  local root=$1 shipped hooks hook installed
  shipped="$(cd "$(dirname "${BASH_SOURCE[0]}")/../hooks" && pwd)/land-guard.pre-push"
  [[ -f "$shipped" ]] || return 0
  hooks=$(git -C "$root" config --get core.hooksPath 2>/dev/null) || hooks=""
  [[ -n "$hooks" ]] || hooks="$(git -C "$root" rev-parse --path-format=absolute --git-common-dir 2>/dev/null)/hooks"
  [[ "$hooks" == /* ]] || hooks="$(git -C "$root" rev-parse --show-toplevel)/$hooks"
  hook="$hooks/pre-push"
  [[ -f "$hook" ]] || return 0
  installed=$(awk '/^# harness-land-guard v[0-9]+ BEGIN$/{k=1} k{print} /^# harness-land-guard v[0-9]+ END$/{k=0}' "$hook")
  [[ -n "$installed" ]] || return 0
  [[ "$installed" == "$(cat "$shipped")" ]] && return 0
  emit_ladder drift fault reinit "installed pre-push land guard ($hook) is a different generation than this lander — run modules/workstation/claude/workflows/hooks/install-land-guard.sh"
  return 3
}

# ── land: the per-project land ORCHESTRATOR (was duplicated in every wrapper) ──────────────────
# All facts arrive as flags so the per-project ship.sh is pure frozen DATA + a delegation line.
# Drives the primitives (preflight|sync-base|deps|land-merge|land-pr|cleanup) on the mode-correct
# path and maps each gate to the agent ladder. Exit 0 done / 20 agent-action-needed / 3 drift|usage.
#   land --root R --base B --mode M [--anchor A] --testcmd T [--depcmd D] -- <branch> <wt> [--assets-ok]
# _promote: the post-verification landing cascade for a given mode (pr|merge-to-main). Single source
# for BOTH direct pr/merge-to-main lands AND the deploy-verify promote step — so the cascade lives
# once. Drives the mode-correct primitives + ladder. Echoes JSON; returns 0 done / 20 action / 3 fault.
#   _promote <mode> <root> <base> <wt> <branch> <testcmd> <depcmd> <assets_ok>
# _mtm_prepare: merge-to-main PRE-land steps (main-side blockers → sync base into branch → reconcile
# deps to the bumped lockfile). Split out so deploy-verify can run it BEFORE deploy, making the head
# e2e verifies equal the head _mtm_land fast-forwards main to. Echoes JSON on a blocker; 0 ready/20/3.
#   _mtm_prepare <root> <base> <wt> <branch> <depcmd>
_mtm_prepare() {
  local root=$1 base=$2 wt=$3 branch=$4 depcmd=$5
  # 1) main-side blockers (dirty main / pending merge) — agent clears, re-runs.
  local pf; pf=$(preflight "$root" "$branch" "$wt") || { echo "$pf" >&2; return 3; }
  if [[ "$(ready_of "$pf")" != "true" ]]; then
    printf '%s\n' "$pf"; emit_ladder preflight blocked clear-blockers "main not clean — see blockers[].recovery, fix, re-run"; return 20
  fi
  # 2) bring branch current with base (resolve-on-branch). Conflict → agent resolves per rubric.
  local sb st; sb=$(sync-base "$root" "$branch" "$base" "$wt") || { echo "$sb" >&2; return 3; }
  st=$(jq_status "$sb")
  if [[ "$st" == "conflict" ]]; then
    printf '%s\n' "$sb"; emit_ladder sync-base conflict resolve-then-rerun "resolve per ~/.claude/workflows/lib/merge-conflict-rubric.md, commit in $wt, re-run"; return 20
  fi
  [[ "$st" == "clean" || "$st" == "up-to-date" ]] || { printf '%s\n' "$sb"; emit_ladder sync-base "$st" investigate "unexpected sync-base status"; return 20; }
  # 2b) reconcile worktree deps to the (possibly base-bumped) lockfile BEFORE the gate — else the
  # gate false-passes on stale node_modules and lands a lockfile-mismatched tree. Idempotent.
  if [[ -n "$depcmd" ]]; then
    local deplog="$wt/.ship-deps.log"
    if ! ( cd "$wt" && eval "$depcmd" ) >"$deplog" 2>&1; then
      emit_ladder deps-refresh failed resolve-then-rerun "dep reconcile ('$depcmd') failed in $wt — read $deplog (full stderr), fix deps, re-run"; return 20
    fi
  fi
  return 0
}
# _mtm_land: merge-to-main POST-verify landing (GATED ff of main to the verified head → ordered
# cleanup). Echoes JSON; returns 0 done / 20 action / 3 fault.
#   _mtm_land <root> <base> <wt> <branch> <testcmd> <assets_ok>
_mtm_land() {
  local root=$1 base=$2 wt=$3 branch=$4 testcmd=$5 assets_ok=$6
  # GATED ff (no markers + clean + ancestor + tests) → main advances to the verified head.
  local lm; lm=$(land-merge "$root" "$branch" "$base" "$wt" "$testcmd") || { echo "$lm" >&2; return 3; }
  if [[ "$(jq_status "$lm")" != "landed" ]]; then
    printf '%s\n' "$lm"; emit_ladder land-merge "$(jq_status "$lm")" fix-then-rerun "land gate not green (tests/markers/dirty/not-ff) — fix in $wt, re-run"; return 20
  fi
  # ordered cleanup: push main → remove worktree → delete branch.
  local cl; cl=$(cleanup "$root" "$branch" "$wt" $assets_ok)
  if [[ "$(jq_status "$cl")" != "cleaned" ]]; then
    printf '%s\n' "$cl"; emit_ladder cleanup "$(jq_status "$cl")" resolve-then-rerun "landed on main but cleanup stalled (assets/push/worktree) — see detail, re-run"; return 20
  fi
  emit_ladder landed done none "merged to $base, pushed, worktree removed, branch deleted"; return 0
}
_promote() {
  local mode=$1 root=$2 base=$3 wt=$4 branch=$5 testcmd=$6 depcmd=$7 assets_ok=$8
  if [[ "$mode" == "pr" ]]; then
    # PR mode: push branch + open PR (NEVER touch main). No preflight (main untouched — a dirty-main
    # WIP must not block a PR) and NO base-sync (pre-merging pollutes the PR diff). If the branch is
    # actually unmergeable, the agent syncs deliberately per the rubric, not the driver by default.
    local pr st; pr=$(land-pr "$root" "$branch" "$base" "$wt" "$testcmd") || { echo "$pr" >&2; return 3; }
    st=$(jq_status "$pr")
    if [[ "$st" != "pr-opened" && "$st" != "pr-exists" ]]; then
      printf '%s\n' "$pr"; emit_ladder land-pr "$st" fix-then-rerun "PR not opened — see detail"; return 20
    fi
    # Cleanup keeps the branch (the PR needs it) and never pushes main. Surface a stalled cleanup
    # (e.g. assets-present) instead of swallowing it — else the worktree leaks silently and the
    # --assets-ok re-entry is unreachable. The PR is already open; re-entry is safe (pr-exists).
    local cl; cl=$(cleanup "$root" "$branch" "$wt" --keep-branch --no-push $assets_ok)
    if [[ "$(jq_status "$cl")" != "cleaned" ]]; then
      printf '%s\n%s\n' "$pr" "$cl"
      emit_ladder cleanup "$(jq_status "$cl")" resolve-then-rerun "PR is OPEN, but worktree cleanup stalled (assets present?) — see detail; preserve assets to <main>/tmp, then re-run with --assets-ok"; return 20
    fi
    printf '%s\n' "$pr"; return 0
  fi

  # merge-to-main path: prepare (blockers→sync→deps) then GATED land+cleanup. Two-step so deploy-verify
  # can reuse prepare BEFORE deploy and land AFTER e2e — same single source for the direct land here.
  if stage_a_enabled "$root"; then
    _stage_a_promote "$root" "$wt" "$branch" "$testcmd" "$depcmd" "$assets_ok"; return $?
  fi
  _mtm_prepare "$root" "$base" "$wt" "$branch" "$depcmd" || return $?
  _mtm_land "$root" "$base" "$wt" "$branch" "$testcmd" "$assets_ok"; return $?
}

land() {
  local root="" base="" mode="" anchor="" testcmd="" depcmd="" deploycmd="" e2ecmd="" promote="" postlandcmd=""
  while [[ $# -gt 0 ]]; do case "$1" in
    --root) root=$2; shift 2;; --base) base=$2; shift 2;; --mode) mode=$2; shift 2;;
    --anchor) anchor=$2; shift 2;; --testcmd) testcmd=$2; shift 2;; --depcmd) depcmd=$2; shift 2;;
    --deploycmd) deploycmd=$2; shift 2;; --e2ecmd) e2ecmd=$2; shift 2;; --promote) promote=$2; shift 2;;
    --postlandcmd) postlandcmd=$2; shift 2;;
    --) shift; break;;
    *) echo "finish-branch land: unknown flag: $1" >&2; return 3;; esac; done
  local branch=${1:-} wt=${2:-}; shift 2 2>/dev/null || true
  local assets_ok=""; [[ "${1:-}" == "--assets-ok" ]] && assets_ok="--assets-ok"
  [[ -n "$root" && -n "$base" && -n "$mode" && -n "$testcmd" && -n "$branch" && -n "$wt" ]] \
    || { echo "usage: finish-branch.sh land --root R --base B --mode M [--anchor A] --testcmd T [--depcmd D] [--postlandcmd C] [--deploycmd C --e2ecmd C --promote pr|merge-to-main] -- <branch> <wt> [--assets-ok]" >&2; return 3; }

  # cleanup removes <wt>. A caller sitting inside it lands fine but is left in a deleted
  # directory, so its next `pwd` fails and reports a failure the land never had. Refuse before
  # anything destructive runs, so exit 0 keeps meaning "clean land".
  local _cwd _wtp
  _cwd=$(pwd -P 2>/dev/null) || _cwd=""
  _wtp=$(cd "$wt" 2>/dev/null && pwd -P) || _wtp="$wt"
  if [[ -n "$_cwd" && "$_cwd/" == "$_wtp/"* ]]; then
    echo "finish-branch land: cwd is inside the worktree this land removes ($_wtp) — run it from outside: cd $root && <lander> land $branch $wt" >&2
    return 3
  fi

  drift --root "$root" --base "$base" --mode "$mode" --anchor "$anchor" || return 3

  # Repo-tracked landing policy: <root>/.land-policy pins the landing mode. Any content other
  # than 'merge-to-main' forbids every merge-to-main promote (direct, stage-a, deploy-verify) —
  # fail-closed at this single entry. Absent file = no restriction.
  if [[ "$mode" == "merge-to-main" || ( "$mode" == "deploy-verify" && "$promote" == "merge-to-main" ) ]] \
    && [[ -f "$root/.land-policy" ]]; then
    local _lp; _lp=$(tr -d '[:space:]' < "$root/.land-policy")
    if [[ "$_lp" != "merge-to-main" ]]; then
      emit_ladder land-policy blocked use-pr-mode "$root/.land-policy pins landing to '$_lp' — merge-to-main is forbidden for this repo; land via --mode pr (push branch, open PR, merge through the repo's PR gate)"
      return 20
    fi
  fi

  # deploy-verify: deploy to a preview env → e2e against the LIVE preview → promote ONLY on green.
  # The deploy + e2e steps PREPEND a verification gate; landing reuses the same _promote cascade.
  if [[ "$mode" == "deploy-verify" ]]; then
    [[ -n "$deploycmd" && -n "$e2ecmd" && -n "$promote" ]] \
      || { echo "finish-branch land: deploy-verify requires --deploycmd, --e2ecmd, --promote pr|merge-to-main" >&2; return 3; }
    [[ "$promote" == "pr" || "$promote" == "merge-to-main" ]] \
      || { echo "finish-branch land: --promote must be pr|merge-to-main, got '$promote'" >&2; return 3; }
    local stage_a_mtm=0 verified_tree=""
    [[ "$promote" == "merge-to-main" ]] && stage_a_enabled "$root" && stage_a_mtm=1
    # promote==merge-to-main: sync base into the branch + reconcile deps BEFORE deploy, so the head we
    # deploy + e2e-verify is the SAME head land-merge fast-forwards main to — NEVER promote an unverified
    # build (a post-e2e sync would advance the landed head past what e2e saw). promote==pr pushes the
    # tested head as-is (no pre-sync — matches land-pr's no-pollute-the-diff contract).
    if [[ "$promote" == "merge-to-main" && "$stage_a_mtm" -eq 0 ]]; then
      _mtm_prepare "$root" "$base" "$wt" "$branch" "$depcmd" || return $?
    fi
    local dp; dp=$(deploy-preview "$wt" "$deploycmd") || { echo "$dp" >&2; return 3; }
    if [[ "$(jq_status "$dp")" != "deployed" ]]; then
      printf '%s\n' "$dp"; emit_ladder deploy-preview "$(jq_status "$dp")" fix-then-rerun "preview deploy did not yield a reachable URL — fix deploy, re-run"; return 20
    fi
    local url; url=$(jq_field "$dp" url)
    if [[ "$stage_a_mtm" -eq 1 ]]; then
      verified_tree=$(git -C "$wt" write-tree 2>/dev/null) || return 3
    fi
    local eg; eg=$(e2e-gate "$wt" "$e2ecmd" "$url") || { echo "$eg" >&2; return 3; }
    if [[ "$(jq_status "$eg")" != "passed" ]]; then
      printf '%s\n%s\n' "$dp" "$eg"; emit_ladder e2e-gate failed fix-then-rerun "e2e failed against preview $url — fix, do NOT promote, re-run"; return 20
    fi
    printf '%s\n%s\n' "$dp" "$eg"
    # land the verified head. merge-to-main: prepare already ran (head is current) → land-only (a second
    # sync would be a no-op but land-merge's ancestor re-check still fail-closes if base moved during e2e).
    if [[ "$stage_a_mtm" -eq 1 ]]; then
      _stage_a_promote "$root" "$wt" "$branch" "$testcmd" "$depcmd" "$assets_ok" "$verified_tree"; return $?
    fi
    if [[ "$promote" == "merge-to-main" ]]; then
      _mtm_land "$root" "$base" "$wt" "$branch" "$testcmd" "$assets_ok"; return $?
    fi
    _promote "$promote" "$root" "$base" "$wt" "$branch" "$testcmd" "$depcmd" "$assets_ok"; return $?
  fi

  local prc; _promote "$mode" "$root" "$base" "$wt" "$branch" "$testcmd" "$depcmd" "$assets_ok"; prc=$?
  [[ $prc -eq 0 ]] || return $prc

  # postlandcmd (merge-to-main only): local push=deploy — runs in root AFTER base is landed+pushed.
  # A failure here is a DEPLOY failure, not a land failure: base is already durable. Fail-closed
  # reporting, but the operator must NOT re-run land (branch is gone) — fix and run the cmd directly.
  if [[ "$mode" == "merge-to-main" && -n "$postlandcmd" ]]; then
    # root IS the deployment source: bring its base checkout up to the just-landed head first
    # (ff-only — a diverged/dirty root is a deploy failure, never guessed around).
    if [[ "$(git -C "$root" branch --show-current 2>/dev/null)" == "$base" ]]; then
      if ! git -C "$root" merge --ff-only --quiet "origin/$base" 2>/dev/null; then
        emit_ladder post-land-deploy failed fix-then-deploy "base LANDED+pushed OK; root $base cannot fast-forward to origin/$base — reconcile root, then run the deploy command directly: $postlandcmd"
        return 20
      fi
    fi
    local plog plrc; plog=$( ( cd "$root" && bash -c "$postlandcmd" ) 2>&1 ); plrc=$?
    if [[ $plrc -ne 0 ]]; then
      printf '%s\n' "$plog" >&2
      emit_ladder post-land-deploy failed fix-then-deploy "base LANDED+pushed OK; local deploy exited $plrc — do NOT re-run land; fix, then run the deploy command directly: $postlandcmd"
      return 20
    fi
    printf '%s\n' "$plog" | tail -1
  fi
  return 0
}

# ── dispatch (only when executed, not when sourced by the test harness) ───────────────────────
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
  cmd=${1:-}; shift || true
  case "$cmd" in
    preflight)      preflight      "$@";;
    sync-base)      sync-base      "$@";;
    land-merge)     land-merge     "$@";;
    land-pr)        land-pr        "$@";;
    deploy-preview) deploy-preview "$@";;
    e2e-gate)       e2e-gate       "$@";;
    cleanup)        cleanup        "$@";;
    land)           land           "$@";;
    drift)          drift          "$@" && emit_ladder drift ok none "frozen facts still hold";;
    *) echo "usage: finish-branch.sh {preflight|sync-base|land-merge|land-pr|deploy-preview|e2e-gate|cleanup|land|drift} ..." >&2; exit 2;;
  esac
fi
