#!/usr/bin/env bash
# packaging/deploy-local.sh — deploy overdeck to the LOCAL services from landed main.
# audience: AI agents. Fail-closed: any delivery or rollback-safety step failing exits non-zero;
# late cleanup and observability tripwires finish as deployed-degraded. Nothing is guessed
# or forced. Idempotent — safe to re-run after a failure.
# Invoked by ship.sh land (postlandcmd) AFTER main is landed+pushed, or directly.
#
# Serves from a standalone deploy clone (~/.local/share/overdeck/deploy), never the dev
# checkout — dev-tree WIP can neither block a deploy nor leak unlanded code into services.
# A docs-only landing short-circuits below; force a full deploy by deleting
# <deploy-clone>/.git/harness-deployed-sha.
set -uo pipefail

DEPLOY="${OVERDECK_DEPLOY_DIR:-$HOME/.local/share/overdeck/deploy}"
export OVERDECK_DEPLOY_DIR="$DEPLOY"
DEPLOY_SCRIPT_PATH=$(readlink -f "${BASH_SOURCE[0]}" 2>/dev/null || true)
[[ -f "$DEPLOY_SCRIPT_PATH" ]] \
  || { printf 'deploy-local: cannot resolve running script identity\n' >&2; exit 1; }
RUNNING_DEPLOY_SCRIPT_DIGEST=$(sha256sum "$DEPLOY_SCRIPT_PATH" | cut -d' ' -f1) \
  || { printf 'deploy-local: cannot digest running script\n' >&2; exit 1; }

# The deploy checkout is a release tree, not an install destination. Between deploys its
# working tree is mechanically read-only; .git remains writable for fetch/inspection.
PERMISSION_LIB="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/lib/release-tree.sh"
[[ -r "$PERMISSION_LIB" ]] || { printf 'deploy-local: missing %s\n' "$PERMISSION_LIB" >&2; exit 1; }
# shellcheck source=lib/release-tree.sh
source "$PERMISSION_LIB"
MAIN_CHECKOUT_SYNC_LIB="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/lib/main-checkout-ff-sync.sh"
[[ -r "$MAIN_CHECKOUT_SYNC_LIB" ]] || { printf 'deploy-local: missing %s\n' "$MAIN_CHECKOUT_SYNC_LIB" >&2; exit 1; }
# shellcheck source=lib/main-checkout-ff-sync.sh
source "$MAIN_CHECKOUT_SYNC_LIB"
READINESS_LIB="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/lib/readiness.sh"
[[ -r "$READINESS_LIB" ]] || { printf 'deploy-local: missing %s\n' "$READINESS_LIB" >&2; exit 1; }
# shellcheck source=lib/readiness.sh
source "$READINESS_LIB"
COMPONENT_RECEIPT_LIB="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/lib/component-receipts.sh"
[[ -r "$COMPONENT_RECEIPT_LIB" ]] || { printf 'deploy-local: missing %s\n' "$COMPONENT_RECEIPT_LIB" >&2; exit 1; }
# shellcheck source=lib/component-receipts.sh
source "$COMPONENT_RECEIPT_LIB"
DELIVERY_MANIFEST_LIB="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/lib/delivery-manifests.sh"
[[ -r "$DELIVERY_MANIFEST_LIB" ]] || { printf 'deploy-local: missing %s\n' "$DELIVERY_MANIFEST_LIB" >&2; exit 1; }
# shellcheck source=lib/delivery-manifests.sh
source "$DELIVERY_MANIFEST_LIB"
COMPONENT_RECEIPT_DIR="${OVERDECK_COMPONENT_RECEIPT_DIR:-${XDG_STATE_HOME:-$HOME/.local/state}/overdeck/deploy-components}"
export COMPONENT_RECEIPT_DIR
DELIVERY_MANIFEST_DIR="${OVERDECK_DELIVERY_MANIFEST_DIR:-${XDG_STATE_HOME:-$HOME/.local/state}/overdeck/delivery-manifests}"
DELIVERY_REQUEST_IDS=()
DELIVERY_DEPLOYMENT_ID=""

collect_delivery_request_ids() {
  delivery_manifest_collect "$DEPLOY" "$DELIVERY_MANIFEST_DIR" "$1" "$2" DELIVERY_REQUEST_IDS
}

emit_deploy_evidence() {
  local stage=$1; shift
  (( ${#DELIVERY_REQUEST_IDS[@]} > 0 )) || return 0
  local writer="$DEPLOY/modules/workstation/claude/hooks/request-delivery-evidence.mjs" request_id
  local -a request_args=()
  [[ -r "$writer" && -x "${OVERDECK_DELIVERY_NODE_BIN:-/usr/bin/node}" ]] || return 1
  for request_id in "${DELIVERY_REQUEST_IDS[@]}"; do request_args+=(--request-id "$request_id"); done
  "${OVERDECK_DELIVERY_NODE_BIN:-/usr/bin/node}" "$writer" --stage "$stage" "${request_args[@]}" "$@"
}

# Re-exec the deploy clone's copy when launched from the SHARED main checkout. That checkout
# sits detached on an old commit and nobody advances it, so its copy of this script drifts
# arbitrarily far behind main — a land that deploys through it silently runs deploy logic from
# weeks ago (no progress state, no owner notice, stale install steps) while reporting green.
# The deploy clone is re-pinned to origin/main every run, so its copy is the current one.
# Scoped deliberately: a worktree copy is an agent's own edit under test and is NEVER hijacked.
if [[ -z "${OVERDECK_DEPLOY_CALLER_REEXEC:-}" ]]; then
  _self="$DEPLOY_SCRIPT_PATH"
  _current="${DEPLOY}/packaging/deploy-local.sh"
  if [[ "$_self" == "$HOME/Projects/overdeck/packaging/deploy-local.sh" \
        && -f "$_current" && -n "$_self" ]] && ! cmp -s "$_self" "$_current"; then
    export OVERDECK_DEPLOY_CALLER_REEXEC=1
    exec bash "$_current" "$@"
  fi
fi

# Own the overall deadline here rather than relying on every caller to remember a wrapper.
# The child owns locks and rollback traps; timeout supervises that complete process tree and
# preserves the conventional exit 124 so queued and direct callers receive the same verdict.
DEPLOY_DEADLINE_SECONDS="${OVERDECK_DEPLOY_DEADLINE_SECONDS:-1200}"
if [[ ! "$DEPLOY_DEADLINE_SECONDS" =~ ^[1-9][0-9]*$ ]]; then
  printf 'FATAL phase=deploy-start invalid-deadline=%s exit=2\n' "$DEPLOY_DEADLINE_SECONDS" >&2
  exit 2
fi
if [[ "${OVERDECK_DEPLOY_DEADLINE_ACTIVE:-0}" != "1" ]]; then
  printf 'phase=deploy-start deadline=%ss\n' "$DEPLOY_DEADLINE_SECONDS" >&2
  OVERDECK_DEPLOY_DEADLINE_ACTIVE=1 /usr/bin/timeout --foreground --signal=TERM --kill-after=15s \
    "${DEPLOY_DEADLINE_SECONDS}s" bash "$DEPLOY_SCRIPT_PATH" "$@"
  deploy_rc=$?
  if [[ $deploy_rc -eq 124 ]]; then
    printf 'FATAL phase=deploy-timeout deadline=%ss exit=124\n' "$DEPLOY_DEADLINE_SECONDS" >&2
  else
    printf 'phase=deploy-finish exit=%s\n' "$deploy_rc" >&2
  fi
  exit "$deploy_rc"
fi

deploy_phase() {
  printf 'phase=%s\n' "$1" >&2
}

if [[ "${OVERDECK_DEPLOY_DRY_RUN:-}" == "1" ]]; then
  printf 'install-gptbridge-links:%s/modules/gptbridge/install.sh --links-only\n' "${DEPLOY}"
  printf 'sync-pi-provider:%s/modules/workstation/pi/agent/models.json\n' "${DEPLOY}"
  printf 'install-user-bin:%s/bin/deckctl sync apply bin\n' "${DEPLOY}"
  printf 'install-workstation-bin:%s/bin/deckctl sync apply claude bin\n' "${DEPLOY}"
  printf 'install-workstation-lib:%s/bin/deckctl sync apply claude lib\n' "${DEPLOY}"
  printf 'install-cloudflare-token-registry:%s/bin/deckctl sync apply claude cloudflare-token-targets.json\n' "${DEPLOY}"
  printf 'install-buildbox-registry:%s/bin/deckctl sync apply claude buildbox-hosts.json\n' "${DEPLOY}"
  printf 'install-agent-slice:%s/modules/monitor/systemd/user/agent.slice → $HOME/.config/systemd/user/agent.slice; systemctl --user daemon-reload\n' "${DEPLOY}"
  printf 'install-session-transcript-converge:%s/modules/workstation/systemd/user/session-transcript-converge.{service,timer} \xe2\x86\x92 $HOME/.config/systemd/user/; systemctl --user enable --now session-transcript-converge.timer\n' "${DEPLOY}"
  printf 'install-request-evidence-drain:%s/modules/workstation/systemd/user/request-evidence-drain.{path,service,timer} -> $HOME/.config/systemd/user/; systemctl --user enable --now request-evidence-drain.path request-evidence-drain.timer\n' "${DEPLOY}"
  printf 'install-deploy-queue:%s/packaging/overdeck-deploy.{path,service} → $HOME/.config/systemd/user/; systemctl --user enable --now overdeck-deploy.path\n' "${DEPLOY}"
  printf 'install-fire-consumer:%s/modules/fire-consumer/overdeck-fire-consumer.{path,service} → $HOME/.config/systemd/user/; systemctl --user enable --now overdeck-fire-consumer.path\n' "${DEPLOY}"
  printf 'install-systray-commands:python3 %s/modules/systray/install.py\n' "${DEPLOY}"
  printf 'install-gateway-grant-renewal:%s/modules/systray/systemd/systray-gateway-grant-renewal.{service,timer} -> $HOME/.config/systemd/user/; systemctl --user enable --now systray-gateway-grant-renewal.timer; start service now\n' "${DEPLOY}"
  printf 'install-kanboard:%s/packaging/install-kanboard.sh\n' "${DEPLOY}"
  printf 'install-controller:%s/packaging/install-controller.sh\n' "${DEPLOY}"
  printf 'install-collector:%s/packaging/install.sh\n' "${DEPLOY}"
  printf 'stage-collector:%s/packaging/stage-backend-release.sh collector <sha>; activate through backend-release.sh with exact-SHA readiness\n' "${DEPLOY}"
  printf 'stage-controller:%s/packaging/stage-backend-release.sh controller <sha>; activate through backend-release.sh with exact-SHA readiness\n' "${DEPLOY}"
  printf 'stage-botmaster-proxy:%s/packaging/stage-backend-release.sh botmaster-proxy <sha>; activate through backend-release.sh with exact-SHA readiness\n' "${DEPLOY}"
  printf 'stage-actions-gateway:%s/packaging/stage-backend-release.sh actions-gateway <sha>; activate through backend-release.sh with exact-SHA authenticated readiness when configured\n' "${DEPLOY}"
  printf 'install-botmaster-proxy:%s/packaging/install-botmaster-proxy.sh\n' "${DEPLOY}"
  printf 'install-botmaster-notify:%s/packaging/install-botmaster-notify.sh\n' "${DEPLOY}"
  printf 'install-web:%s/packaging/install-web.sh\n' "${DEPLOY}"
  printf 'install-buildbox-parity:%s/packaging/install-buildbox-parity.sh\n' "${DEPLOY}"
  printf 'install-actions-gateway:%s/packaging/install-actions-gateway.sh; immutable current/bin/start; configured instances activate with exact-SHA authenticated smoke, absent config stays disabled\n' "${DEPLOY}"
  printf 'brief-canary:%s/collector/scripts/brief-canary.ts\n' "${DEPLOY}"
  printf 'sandbox-image-parity:%s/modules/workstation/claude/bin/sandbox-provision --check --all\n' "${DEPLOY}"
  printf 'restart-group:overdeck-web.service overdeck-web-watchdog.service\n'
  exit 0
fi

WEB_URL="http://127.0.0.1:31337"
COLLECTOR_URL="http://127.0.0.1:31338"
COLLECTOR_BACKEND_URL="${OVERDECK_COLLECTOR_BACKEND_URL:-http://127.0.0.1:31341}"
CONTROLLER_BACKEND_URL="${OVERDECK_CONTROLLER_BACKEND_URL:-http://127.0.0.1:8787}"
BOTMASTER_PROXY_URL="${OVERDECK_BOTMASTER_PROXY_URL:-http://127.0.0.1:31340}"
ACTIONS_GATEWAY_URL="${OVERDECK_ACTIONS_GATEWAY_URL:-http://127.0.0.1:31401}"
COLLECTOR_TOKEN_FILE="${OVERDECK_COLLECTOR_TOKEN_FILE:-$HOME/.config/overdeck/token}"
CONTROLLER_TOKEN_FILE="${OVERDECK_CONTROLLER_TOKEN_FILE:-$HOME/.config/overdeck/token}"
BACKEND_RELEASE_ROOT="${OVERDECK_BACKEND_RELEASE_ROOT:-${XDG_STATE_HOME:-$HOME/.local/state}/overdeck/backend}"
export OVERDECK_BACKEND_RELEASE_ROOT="$BACKEND_RELEASE_ROOT"
# Sized from measurement, not hope: a cold collector answered after 151s under normal
# load (2026-08-16). A single monotonic deadline bounds the complete typed probe loop;
# attempt counts made the real budget vary with curl duration and wall-clock scheduling.
# Env-overridable for the test suite only; production never sets these.
SMOKE_READINESS_TIMEOUT="${OVERDECK_SMOKE_READINESS_TIMEOUT:-300}"
SMOKE_READINESS_DELAY="${OVERDECK_SMOKE_READINESS_DELAY:-3}"
SERVICES=(overdeck-collector.service overdeck-web.service overdeck-controller.service botmaster-proxy.service overdeck-web-watchdog.service)
ACTIONS_GATEWAY_SERVICE=actions-gateway.service
ACTIONS_GATEWAY_CONFIG="$HOME/.config/overdeck/actions-gateway.env"

# This script is the local immutable-release builder. Its own `pnpm` calls must not
# re-enter the session PATH shim: the shim turns builds into remote-only work after
# this script installs the latest workstation commands, which makes an in-progress
# local deployment depend on an unrelated buildbox. Resolve the real binary once,
# rejecting the shim directory and any missing candidate.
resolve_real_pnpm() {
  local shim_dir="$HOME/.claude/bin" candidate resolved
  local -a path_parts
  IFS=':' read -r -a path_parts <<< "$PATH"
  for candidate in "${path_parts[@]}"; do
    [[ "$candidate" == "$shim_dir" ]] && continue
    [[ -x "$candidate/pnpm" && ! -d "$candidate/pnpm" ]] || continue
    resolved=$(readlink -f "$candidate/pnpm" 2>/dev/null) || continue
    [[ -x "$resolved" && "$resolved" != "$shim_dir/"* \
      && "${resolved##*/}" != '_cpu-guard-shim.sh' \
      && "${resolved##*/}" != 'pnpm-entrypoint' ]] || continue
    printf '%s\n' "$resolved"
    return 0
  done
  return 1
}
without_session_shims() {
  local shim_dir="$HOME/.claude/bin" candidate node_target result=""
  local -a path_parts
  IFS=':' read -r -a path_parts <<< "$PATH"
  for candidate in "${path_parts[@]}"; do
    [[ "$candidate" == "$shim_dir" ]] && continue
    node_target=$(readlink -f "$candidate/node" 2>/dev/null || true)
    [[ "${node_target##*/}" == '_cpu-guard-shim.sh' ]] && continue
    result="${result:+${result}:}${candidate}"
  done
  printf '%s\n' "$result"
}
PNPM_BIN="$(resolve_real_pnpm)" || {
  printf 'deploy-local: cannot resolve a real pnpm outside %s\n' "$HOME/.claude/bin" >&2
  exit 1
}
PNPM_EXEC_PATH="$(without_session_shims)"
NODE_BIN=$(PATH="$PNPM_EXEC_PATH" command -v node 2>/dev/null || true)
NODE_BIN=$(readlink -f "$NODE_BIN" 2>/dev/null || true)
[[ -x "$NODE_BIN" && "$NODE_BIN" != "$HOME/.claude/bin/"* \
  && "${NODE_BIN##*/}" != '_cpu-guard-shim.sh' ]] || {
  printf 'deploy-local: cannot resolve a real node outside %s\n' "$HOME/.claude/bin" >&2
  exit 1
}
PNPM_USERCONFIG="${DEPLOY}/.npmrc"
CURL_BIN="${OVERDECK_DEPLOY_CURL_BIN:-/usr/bin/curl}"
run_pnpm() {
  env CI=true PATH="$PNPM_EXEC_PATH" NPM_CONFIG_USERCONFIG="$PNPM_USERCONFIG" \
    "$NODE_BIN" "$PNPM_BIN" "$@"
}
verify_private_registry() {
  local registry spec tarball_json tarball
  [[ -r "$PNPM_USERCONFIG" ]] || {
    printf 'deploy-local: cannot read pnpm registry configuration at %s\n' "$PNPM_USERCONFIG" >&2
    return 1
  }
  [[ -x "$CURL_BIN" ]] || {
    printf 'deploy-local: cannot execute registry probe curl at %s\n' "$CURL_BIN" >&2
    return 1
  }
  registry=$(run_pnpm config get '@platform-modules:registry') || return 1
  registry="${registry%/}/"
  [[ "$registry" == http://100.101.104.41:4873/ ]] || {
    printf 'deploy-local: unexpected @platform-modules registry: %s\n' "$registry" >&2
    return 1
  }
  for spec in '@platform-modules/query-react@0.1.0' '@platform-modules/ui-tokens@0.2.0'; do
    tarball_json=$(/usr/bin/timeout 30s env PATH="$PNPM_EXEC_PATH" \
      NPM_CONFIG_USERCONFIG="$PNPM_USERCONFIG" "$NODE_BIN" "$PNPM_BIN" \
      view "$spec" dist.tarball --json) || return 1
    tarball=$(python3 -c 'import json, sys; value = json.load(sys.stdin); print(value if isinstance(value, str) else "")' <<<"$tarball_json") \
      || return 1
    [[ "$tarball" == "${registry}tarballs/"* ]] || {
      printf 'deploy-local: %s published an unexpected tarball URL: %s\n' "$spec" "$tarball" >&2
      return 1
    }
    "$CURL_BIN" --fail --silent --show-error --head --connect-timeout 5 --max-time 20 "$tarball" >/dev/null \
      || return 1
    printf 'registry-ok package=%s metadata=verified tarball=reachable\n' "$spec" >&2
  done
}

DEPLOY_STATE_FILE="${OVERDECK_DEPLOY_STATE_FILE:-${XDG_STATE_HOME:-$HOME/.local/state}/overdeck/deploy-status.json}"
BACKEND_RELEASE_REPORT="${OVERDECK_BACKEND_RELEASE_REPORT:-${DEPLOY_STATE_FILE%/*}/backend-releases.json}"
BACKEND_RELEASE_MINIMUM_AGE="${OVERDECK_BACKEND_RELEASE_MINIMUM_AGE:-86400}"
DEPLOY_STARTED_AT="${OVERDECK_DEPLOY_STARTED_AT:-$(date +%s)}"
export OVERDECK_DEPLOY_STARTED_AT="$DEPLOY_STARTED_AT"
DEGRADED_REASONS=()

# notify_owner <text> — one Telegram line through botmaster. Fail-open and bounded: a
# notification is observability, never a delivery step, and must not hold a deploy open.
notify_owner() {
  [[ "${OVERDECK_DEPLOY_NOTIFY:-1}" == "1" ]] || return 0
  # Absolute path, never `command -v`: a deploy launched from systemd-run or a hook does
  # not carry the shell's PATH, and a missed lookup would silently disable notifications
  # while the deploy still reports green — the exact failure class this notice exists for.
  local bin="${HOME}/.local/bin/botmaster"
  [[ -x "$bin" ]] || return 0
  timeout 20 "$bin" "$1" >/dev/null 2>&1 || true
}

# what_landed — the commits this deploy installs, in owner language: subject lines plus the
# branch each merge names. Empty when the range cannot be resolved; never guessed.
what_landed() {
  local prev=$1 subjects branches count
  git -C "$DEPLOY" rev-parse --quiet --verify "$prev^{commit}" >/dev/null 2>&1 || return 0
  count=$(git -C "$DEPLOY" rev-list --count --no-merges "$prev..HEAD" 2>/dev/null) || return 0
  [[ "$count" -gt 0 ]] 2>/dev/null || return 0
  subjects=$(git -C "$DEPLOY" log --no-merges --format='• %s' "$prev..HEAD" 2>/dev/null | head -8)
  [[ "$count" -gt 8 ]] && subjects="$subjects"$'\n'"• …and $((count - 8)) more"
  branches=$(git -C "$DEPLOY" log --merges --format='%s' "$prev..HEAD" 2>/dev/null \
    | grep -oE "wt/[A-Za-z0-9._-]+" | sort -u | paste -sd', ')
  printf '%s' "$subjects"
  [[ -n "$branches" ]] && printf '\nFrom: %s' "$branches"
  return 0
}

# deploy_state <state> <step> [detail] — publish progress for readers. Atomic (tmp+mv)
# so a reader never sees a half-written file. A failed write is reported and never
# aborts the deploy: status is observability, not a delivery step.
deploy_state() {
  local state=$1 step=$2 detail=${3:-} tmp now sha="" target_sha="" failure_class="none" main_checkout_sync="${MAIN_CHECKOUT_FF_SYNC_STATUS:-not-attempted}"
  now=$(date +%s)
  # Before the clone is re-pinned its HEAD is still the PREVIOUS deploy, so a caller that
  # already knows the commit it is installing supplies it.
  sha="${DEPLOY_STATE_SHA:-}"
  [[ -n "$sha" ]] || sha=$(cd "$DEPLOY" 2>/dev/null && git rev-parse --short HEAD 2>/dev/null) || sha=""
  target_sha="${OVERDECK_DEPLOY_TARGET_SHA:-}"
  [[ "$target_sha" =~ ^[0-9a-f]{40}$ ]] || target_sha=""
  if [[ "$state" == "failed" ]]; then
    failure_class="${DEPLOY_FAILURE_CLASS:-permanent}"
    [[ "$failure_class" == "none" || "$failure_class" == "transient" || "$failure_class" == "permanent" ]] || failure_class="permanent"
  fi
  mkdir -p "${DEPLOY_STATE_FILE%/*}" 2>/dev/null || { printf 'deploy-local: cannot create deploy status directory\n' >&2; return 0; }
  tmp="$DEPLOY_STATE_FILE.$$.tmp"
  printf '{"schema":2,"state":"%s","step":"%s","detail":"%s","sha":"%s","target_sha":"%s","failure_class":"%s","main_checkout_sync":"%s","pid":%s,"started_at":%s,"updated_at":%s}\n' \
    "$state" "$step" "${detail//\"/\'}" "$sha" "$target_sha" "$failure_class" "$main_checkout_sync" "$$" "$DEPLOY_STARTED_AT" "$now" >"$tmp" \
    && mv "$tmp" "$DEPLOY_STATE_FILE" \
    || { rm -f "$tmp"; printf 'deploy-local: cannot publish deploy status to %s\n' "$DEPLOY_STATE_FILE" >&2; }
  return 0
}

failure_class_for_step() {
  case "$1" in
    # Bounded retries can plausibly recover these host/network/runtime availability seams
    # without changing source or operator-owned configuration. Everything else is a
    # deterministic or unknown blocker and therefore fails closed as permanent.
    deploy-lock-timeout) printf 'none\n' ;;
    fetch-failed|disk-probe-failed|actions-gateway-readiness-failed)
      printf 'transient\n'
      ;;
    *) printf 'permanent\n' ;;
  esac
}

deploy_request_is_covered() {
  local value=$1 target=$2
  [[ -z "$value" || "$value" == "$target" ]] && return 0
  [[ "$value" =~ ^[0-9a-f]{40}$ ]] || return 1
  [[ -d "$DEPLOY/.git" ]] || return 1
  git -C "$DEPLOY" merge-base --is-ancestor "$value" "$target" 2>/dev/null
}

defer_failed_deploy_requests() {
  local target="${OVERDECK_DEPLOY_TARGET_SHA:-}" queue deferred request value destination
  [[ "${DEPLOY_NOW:-0}" == 1 && "$target" =~ ^[0-9a-f]{40}$ ]] || return 0
  queue="${QUEUE:-${OVERDECK_DEPLOY_QUEUE_DIR:-${DEPLOY}-queue}}"
  [[ -d "$queue" ]] || return 0
  deferred="${OVERDECK_DEPLOY_DEFERRED_QUEUE_DIR:-${queue}-deferred}"
  mkdir -p "$deferred" || return 1
  shopt -s nullglob
  for request in "$queue"/req-*; do
    value=$(<"$request") || continue
    deploy_request_is_covered "$value" "$target" || continue
    destination="$deferred/$(basename "$request")"
    [[ ! -e "$destination" ]] || { shopt -u nullglob; return 1; }
    mv -- "$request" "$destination" || { shopt -u nullglob; return 1; }
  done
  shopt -u nullglob
}

acknowledge_deferred_deploy_requests() {
  local target=$1 queue deferred request value
  queue="${QUEUE:-${OVERDECK_DEPLOY_QUEUE_DIR:-${DEPLOY}-queue}}"
  deferred="${OVERDECK_DEPLOY_DEFERRED_QUEUE_DIR:-${queue}-deferred}"
  [[ -d "$deferred" ]] || return 0
  shopt -s nullglob
  for request in "$deferred"/req-*; do
    value=$(<"$request") || continue
    deploy_request_is_covered "$value" "$target" || continue
    rm -f -- "$request" || { shopt -u nullglob; return 1; }
  done
  shopt -u nullglob
}

mark_degraded() {
  local reason=$1 detail=${2:-} safe_detail
  safe_detail=$(printf '%s' "$detail" | tr '\n"' " _" | cut -c1-300)
  DEGRADED_REASONS+=("$reason")
  printf '{"stage":"deploy-local","status":"degraded","reason":"%s","detail":"%s"}\n' \
    "$reason" "$safe_detail" >&2
}

degraded_summary() {
  local IFS=,
  printf '%s' "${DEGRADED_REASONS[*]:-none}"
}

publish_backend_release_report() {
  local gateway_expected=inactive spec component service expected prune_output prune_rc
  local inspect_output inspect_rc tmp first=1
  local -a inspections=()
  (( gateway_enabled )) && gateway_expected=active
  local -a specs=(
    'collector|overdeck-collector.service|active'
    'controller|overdeck-controller.service|active'
    'botmaster-proxy|botmaster-proxy.service|active'
    "actions-gateway|$ACTIONS_GATEWAY_SERVICE|$gateway_expected"
  )

  for spec in "${specs[@]}"; do
    IFS='|' read -r component service expected <<<"$spec"
    prune_output=$(OVERDECK_BACKEND_RELEASE_ROOT="$BACKEND_RELEASE_ROOT" \
      bash "${DEPLOY}/packaging/backend-release.sh" prune "$component" "$BACKEND_RELEASE_MINIMUM_AGE")
    prune_rc=$?
    [[ -n "$prune_output" ]] && printf '%s\n' "$prune_output"
    (( prune_rc == 0 )) \
      || mark_degraded "backend-prune-$component" "retired $component releases could not be pruned safely"

    inspect_output=$(OVERDECK_BACKEND_RELEASE_ROOT="$BACKEND_RELEASE_ROOT" \
      bash "${DEPLOY}/packaging/backend-release.sh" inspect "$component" "$service" "$expected")
    inspect_rc=$?
    if [[ -z "$inspect_output" || "$inspect_output" != \{* ]]; then
      inspect_output=$(printf '{"schema":1,"component":"%s","service":"%s","expected":"%s","state":"unavailable","rollback_ready":false}' \
        "$component" "$service" "$expected")
    fi
    inspections+=("$inspect_output")
    (( inspect_rc == 0 )) \
      || mark_degraded "backend-inspection-$component" "$component release identity or rollback readiness is incomplete"
  done

  mkdir -p "${BACKEND_RELEASE_REPORT%/*}" \
    || { mark_degraded backend-report-write-failed "cannot create backend release report directory"; return 0; }
  tmp="$BACKEND_RELEASE_REPORT.$$.tmp"
  {
    printf '{"schema":1,"deployed_sha":"%s","components":[' "$deployment_sha"
    for inspect_output in "${inspections[@]}"; do
      (( first )) || printf ','
      printf '%s' "$inspect_output"
      first=0
    done
    printf ']}\n'
  } >"$tmp" && mv "$tmp" "$BACKEND_RELEASE_REPORT" \
    || { rm -f "$tmp"; mark_degraded backend-report-write-failed "cannot publish $BACKEND_RELEASE_REPORT"; }
}

fail() {
  local step=$1 detail=$2 failure_class="${3:-}"
  if [[ "$failure_class" != "none" && "$failure_class" != "transient" && "$failure_class" != "permanent" ]]; then
    failure_class=$(failure_class_for_step "$step")
  fi
  if [[ "$failure_class" != "none" ]] && ! defer_failed_deploy_requests; then
    detail+="; failed request could not be deferred — queue consumer remains blocked"
  fi
  DEPLOY_FAILURE_CLASS="$failure_class" deploy_state failed "$step" "$detail"
  if [[ -n "$DELIVERY_DEPLOYMENT_ID" && "${deployment_sha:-}" =~ ^[0-9a-f]{40}$ ]]; then
    emit_deploy_evidence deploy-result --event-key "deploy:${deployment_sha}:${DEPLOY_STARTED_AT}:result" \
      --deployment-id "$DELIVERY_DEPLOYMENT_ID" --release "$deployment_sha" --status failed \
      --readiness "failed:$step" \
      || printf 'deploy-local: exact failed-deployment evidence could not be recorded\n' >&2
  fi
  printf '{"stage":"deploy-local","status":"%s","detail":"%s","failure_class":"%s"}\n' "$step" "$detail" "$failure_class" >&2
  # Edge-triggered: one notification per failure crossing. A different step is a
  # different crossing; a success in between (which removes the marker) re-arms it.
  local edge="${DEPLOY_STATE_FILE}.last-notified-failure"
  if [[ ! -f "$edge" || "$(cat "$edge" 2>/dev/null)" != "$step" ]]; then
    printf '%s\n' "$step" >"$edge" 2>/dev/null || true
    notify_owner "Deploy FAILED at ${step}: ${detail}"
  fi
  exit 1
}

publish_deploy_stamp() {
  printf '%s\n' "$deployment_sha" >"$DEPLOY_STAMP.tmp" \
    && mv "$DEPLOY_STAMP.tmp" "$DEPLOY_STAMP"
}

acknowledge_deploy_requests() {
  (( $# == 0 )) || rm -f -- "$@"
}

# The queue snapshot is durable request coverage. Keep it until every fail-closed
# runtime transition has committed. Late cleanup/observability failures are recorded as
# deployed-degraded and acknowledge the already-installed request instead of redoing it.
finalize_deploy_success() {
  od-live-report-refresh \
    || mark_degraded live-report-refresh-failed "od-live-report-refresh failed after runtime activation"
  [[ "$deployment_sha" =~ ^[0-9a-f]{40}$ ]] \
    || fail deploy-stamp-failed "resolved commit is not a full sha — refusing to stamp: $deployment_sha"
  publish_deploy_stamp \
    || fail deploy-stamp-failed "cannot record the deployed commit stamp"
  deploy_tree_readonly \
    || fail deploy-tree-lock-failed "deployment succeeded but the release tree could not be made read-only"
  main_checkout_ff_sync
  acknowledge_deploy_requests "${queue_snapshot[@]}" \
    || fail deploy-queue-drain-failed "cannot acknowledge completed deploy requests"
  acknowledge_deferred_deploy_requests "$OVERDECK_DEPLOY_TARGET_SHA" \
    || fail deploy-queue-drain-failed "cannot acknowledge deferred deploy requests"
}

# Single standing consumer (overdeck-deploy.service, launched by overdeck-deploy.path
# when the queue is non-empty) owns every real deploy. Every other invocation — a hand
# caller, the controller watcher — enqueues a coalescing request and exits immediately;
# it never takes the lock or runs the build/install below itself. Fail-closed: an
# invocation context this script does not recognize as the consumer enqueues, it never
# silently deploys. `--now` is the explicit emergency/bootstrap escape hatch.
DEPLOY_NOW=0
if [[ "${1:-}" == "--now" ]]; then
  DEPLOY_NOW=1
  shift
elif [[ "${OVERDECK_DEPLOY_CONSUMER:-}" == "1" ]]; then
  DEPLOY_NOW=1
fi
if [[ "$DEPLOY_NOW" == 1 ]]; then
  # Carried through the flock re-invocation (line ~176) and the clone re-exec (line
  # ~272) below — both spawn a FRESH process that would otherwise re-classify itself
  # as a hand caller and demote a real deploy to an enqueue mid-flight.
  export OVERDECK_DEPLOY_CONSUMER=1
else
  enqueue_dir="${OVERDECK_DEPLOY_QUEUE_DIR:-${DEPLOY}-queue}"
  mkdir -p "$enqueue_dir" || fail deploy-queue-failed "cannot create deploy queue at $enqueue_dir"
  enqueue_target="${OVERDECK_DEPLOY_TARGET_SHA:-}"
  if [[ ! "$enqueue_target" =~ ^[0-9a-f]{40}$ ]]; then
    if [[ ! -d "$DEPLOY/.git" ]] \
      || ! git -C "$DEPLOY" fetch --quiet origin main \
      || ! enqueue_target=$(git -C "$DEPLOY" rev-parse --verify refs/remotes/origin/main 2>/dev/null) \
      || [[ ! "$enqueue_target" =~ ^[0-9a-f]{40}$ ]]; then
      printf '{"stage":"deploy-local","status":"deploy-request-failed","detail":"cannot resolve an exact origin/main target"}\n' >&2
      exit 1
    fi
  fi
  enqueue_file="$enqueue_dir/req-$$-$RANDOM"
  printf '%s\n' "$enqueue_target" >"$enqueue_file" \
    || fail deploy-queue-failed "cannot enqueue deploy request at $enqueue_file"
  # deploy-status.json is the shared state machine the standing consumer and the
  # collector's /ci panel read — a hand caller must never write to it, or it clobbers
  # an in-flight consumer's "running" record with a stale "queued" one.
  printf 'deploy requested for %s; the standing deployer will install that landed commit — watch %s\n' \
    "${enqueue_target:0:7}" "$DEPLOY_STATE_FILE" >&2
  printf '{"stage":"deploy-local","status":"deploy-requested","request":"%s","target_sha":"%s"}\n' \
    "$enqueue_file" "$enqueue_target"
  exit 0
fi

QUEUE="${OVERDECK_DEPLOY_QUEUE_DIR:-${DEPLOY}-queue}"
mkdir -p "$QUEUE" || fail deploy-queue-failed "cannot create deploy queue at $QUEUE"

# Bind this consumer run to one requested commit before any admission check can fail.
# New requests always contain a full sha. Empty legacy requests and explicit --now calls
# must fetch first: resolving the existing remote-tracking ref would silently deploy stale
# code when origin/main moved after the deploy clone's previous fetch.
if [[ ! "${OVERDECK_DEPLOY_TARGET_SHA:-}" =~ ^[0-9a-f]{40}$ ]]; then
  newest_target_request=""
  shopt -s nullglob
  for request in "$QUEUE"/req-*; do
    [[ -z "$newest_target_request" || "$request" -nt "$newest_target_request" ]] \
      && newest_target_request="$request"
  done
  shopt -u nullglob
  if [[ -n "$newest_target_request" ]]; then
    OVERDECK_DEPLOY_TARGET_SHA=$(<"$newest_target_request")
  fi
fi
if [[ ! "${OVERDECK_DEPLOY_TARGET_SHA:-}" =~ ^[0-9a-f]{40}$ ]]; then
  [[ -d "$DEPLOY/.git" ]] \
    || fail deployment-target-missing "no valid full target sha in the deploy request"
  ( cd "$DEPLOY" && git fetch --quiet origin main ) \
    || fail fetch-failed "git fetch origin main failed while selecting the deploy target"
  OVERDECK_DEPLOY_TARGET_SHA=$(cd "$DEPLOY" && git rev-parse --verify refs/remotes/origin/main 2>/dev/null || true)
fi
[[ "${OVERDECK_DEPLOY_TARGET_SHA:-}" =~ ^[0-9a-f]{40}$ ]] \
  || fail deployment-target-missing "no valid full target sha in the deploy request"
export OVERDECK_DEPLOY_TARGET_SHA
deploy_state running target-selected "selected immutable deploy target"

floor_mb="${OVERDECK_DEPLOY_MIN_FREE_MB:-15360}"
# df fails on a path that does not exist yet (first deploy) or is mid-rename, so walk up to
# the nearest existing ancestor — same filesystem, same answer.
probe_dir="${DEPLOY}"
while [[ -n "$probe_dir" && ! -d "$probe_dir" ]]; do probe_dir="$(dirname "$probe_dir")"; done
avail_mb=$(df -BM --output=avail "${probe_dir:-/}" 2>/dev/null | tail -1 | tr -dc 0-9)
if [[ ! "$avail_mb" =~ ^[0-9]+$ ]]; then
  fail disk-probe-failed "cannot read free space for ${probe_dir:-/} — df gave no usable answer; this is a probe fault, not a full disk"
fi
[[ "$avail_mb" -ge "$floor_mb" ]] \
  || fail disk-floor "${avail_mb}MB free < ${floor_mb}MB floor; a deploy stages multi-GB snapshots — free space first (override: OVERDECK_DEPLOY_MIN_FREE_MB)"

DEPLOY_STAMP="$DEPLOY/.git/harness-deployed-sha"
# Prints the stamp sha and returns 0 only when every path changed between the last
# fully-successful deploy and $1 is documentation. Runs from $DEPLOY, read-only. Fails
# in every other direction: absent/malformed stamp, unknown base commit, failed diff,
# any single non-doc path.
docs_only_since_stamp() (
  local target=$1 stamp changed line
  cd "$DEPLOY" 2>/dev/null || return 1
  stamp=$(cat "$DEPLOY_STAMP" 2>/dev/null) || return 1
  [[ "$stamp" =~ ^[0-9a-f]{40}$ ]] || return 1
  git cat-file -e "${stamp}^{commit}" 2>/dev/null || return 1
  changed=$(git diff --no-renames --name-only "$stamp" "$target") || return 1
  while IFS= read -r line; do
    [[ -n "$line" ]] || continue
    case "$line" in
      docs/*) ;;
      */*) return 1 ;;
      *.md) ;;
      *) return 1 ;;
    esac
  done <<<"$changed"
  printf '%s\n' "$stamp"
)

# The collector takes ~35-55s to rebind after a restart, and most deploys never touch
# it — a web-only landing pays that downtime for nothing. Returns 0 only when every
# input that feeds the RUNNING collector process is byte-identical between the last
# fully-successful deploy and $2 AND the service is currently active. Fails in every
# other direction (absent/malformed stamp, unknown base commit, failed diff, inactive
# service): a wrong skip leaves the owner on stale code, a wrong restart costs 55s.
COLLECTOR_RESTART_PATHS=(
  collector/
  packages/activity-contract/
  packages/report-contract/
  pnpm-lock.yaml
  pnpm-workspace.yaml
  modules/workstation/claude/hooks/lib/decision-marker.mjs
  modules/workstation/claude/lib/buildbox-registry.mjs
  packaging/deploy-local.sh
  packaging/overdeck-collector.service
  packaging/overdeck-collector-startup-boost.service
  packaging/install.sh
  packaging/stage-backend-release.sh
  modules/workstation/systemd/user/overdeck-collector.service.d/
  modules/monitor/systemd/user/memcap-dropins/overdeck-collector.conf
)
collector_unchanged_since_stamp() (
  local stamp=$1 target=$2
  cd "$DEPLOY" 2>/dev/null || return 1
  [[ "$stamp" =~ ^[0-9a-f]{40}$ ]] || return 1
  git cat-file -e "${stamp}^{commit}" 2>/dev/null || return 1
  systemctl --user is-active --quiet overdeck-collector.service || return 1
  git diff --quiet --no-renames "$stamp" "$target" -- "${COLLECTOR_RESTART_PATHS[@]}"
)

CONTROLLER_RESTART_PATHS=(
  controller/
  package.json
  pnpm-lock.yaml
  pnpm-workspace.yaml
  packaging/overdeck-controller.service
  packaging/install-controller.sh
  packaging/stage-backend-release.sh
)
controller_unchanged_since_stamp() (
  local stamp=$1 target=$2
  cd "$DEPLOY" 2>/dev/null || return 1
  [[ "$stamp" =~ ^[0-9a-f]{40}$ ]] || return 1
  git cat-file -e "${stamp}^{commit}" 2>/dev/null || return 1
  systemctl --user is-active --quiet overdeck-controller.service || return 1
  git diff --quiet --no-renames "$stamp" "$target" -- "${CONTROLLER_RESTART_PATHS[@]}"
)

BOTMASTER_PROXY_RESTART_PATHS=(
  packaging/botmaster-proxy.ts
  packaging/botmaster-proxy.service
  packaging/install-botmaster-proxy.sh
  packaging/stage-backend-release.sh
  modules/botmaster/notify/
)

ACTIONS_GATEWAY_RESTART_PATHS=(
  modules/actions-gateway/
  tsconfig.json
  packaging/actions-gateway.service
  packaging/install-actions-gateway.sh
  packaging/stage-backend-release.sh
)

service_unchanged_since_stamp() (
  local service=$1 stamp=$2 target=$3
  shift 3
  cd "$DEPLOY" 2>/dev/null || return 1
  [[ "$stamp" =~ ^[0-9a-f]{40}$ ]] || return 1
  git cat-file -e "${stamp}^{commit}" 2>/dev/null || return 1
  systemctl --user is-active --quiet "$service" || return 1
  git diff --quiet --no-renames "$stamp" "$target" -- "$@"
)

keep_service_dark() {
  local service=$1
  systemctl --user stop "$service" >/dev/null 2>&1 || true
  systemctl --user disable "$service" >/dev/null 2>&1 || true
  ! systemctl --user is-active --quiet "$service" \
    && ! systemctl --user is-enabled --quiet "$service"
}

PNPM_INSTALL_PATHS=(pnpm-lock.yaml pnpm-workspace.yaml package.json '*.npmrc' '**/package.json')
web_build_dependencies_resolve() (
  cd "$DEPLOY" 2>/dev/null || return 1
  "$NODE_BIN" <<'JS'
const { createRequire } = require("node:module");
const { resolve } = require("node:path");
const fromAstroConfig = createRequire(resolve("apps/web/astro.config.mjs"));
for (const specifier of ["astro", "@astrojs/node", "@astrojs/react", "@tailwindcss/vite"]) {
  fromAstroConfig.resolve(specifier);
}
JS
)
pnpm_install_unchanged_since_stamp() (
  local stamp=$1 target=$2 marker
  cd "$DEPLOY" 2>/dev/null || return 1
  [[ "$stamp" =~ ^[0-9a-f]{40}$ ]] || return 1
  git cat-file -e "${stamp}^{commit}" 2>/dev/null || return 1
  [[ -d node_modules && -d node_modules/.pnpm ]] || return 1
  # `pnpm deploy --prod` can leave the shared clone's manifest production-only after an
  # interrupted run. Source inputs may be unchanged while the next web build is missing
  # Tailwind/Astro build dependencies, so that state is not converged.
  python3 -c 'import json, sys; data = json.load(open(sys.argv[1])); raise SystemExit(0 if data.get("included", {}).get("devDependencies") is True else 1)' \
    node_modules/.modules.yaml 2>/dev/null || return 1
  for marker in node_modules/.pnpm-debug.log node_modules/.pnpm-lock.yaml.tmp node_modules/.installing; do
    [[ ! -e "$marker" ]] || return 1
  done
  git diff --quiet --no-renames "$stamp" "$target" -- "${PNPM_INSTALL_PATHS[@]}" \
    && web_build_dependencies_resolve
)

REAPER_RESTART_PATHS=(
  modules/workstation/claude/bin/reaper-notifier.py
  'modules/workstation/claude/bin/_agent_reaper_lib*'
  modules/workstation/claude/bin/resolve-session.py
  modules/workstation/claude/bin/reaper-ctl
  modules/workstation/systemd/user/reaper-notifier.service
)

# One deploy at a time: every step below mutates the single shared clone and the live
# services, and the land queue releases its conductor lock BEFORE postlandcmd, so lands
# that queued together reach this script at once. The lock sits outside the clone, which
# has to stay pristine for the dirty check below.
LOCK="${OVERDECK_DEPLOY_LOCK:-${DEPLOY}.lock}"
LOCK_WAIT="${OVERDECK_DEPLOY_LOCK_WAIT:-1800}"

# The lock serializes multi-minute web builds, so a docs-only landing that waits for it
# pays another lane's build time to install nothing. Classify BEFORE taking it: fetch and
# diff touch no working tree, so this is safe while another deploy holds the lock. The
# queue still drains here — a docs-only caller satisfies every pending request exactly as
# the locked path does. Fail closed: a failed fetch or any non-doc path falls through.
if [[ -d "$DEPLOY/.git" ]] && ( cd "$DEPLOY" && git fetch --quiet origin ) 2>/dev/null \
  && git -C "$DEPLOY" cat-file -e "${OVERDECK_DEPLOY_TARGET_SHA}^{commit}" 2>/dev/null \
  && prelock_stamp=$(docs_only_since_stamp "$OVERDECK_DEPLOY_TARGET_SHA"); then
  prelock_sha="${OVERDECK_DEPLOY_TARGET_SHA:0:7}"
  prelock_queued=()
  shopt -s nullglob
  for request in "$QUEUE"/req-*; do
    request_target=$(<"$request")
    deploy_request_is_covered "$request_target" "$OVERDECK_DEPLOY_TARGET_SHA" \
      && prelock_queued+=("$request")
  done
  shopt -u nullglob
  acknowledge_deploy_requests "${prelock_queued[@]}" \
    || fail deploy-queue-drain-failed "cannot acknowledge completed deploy requests"
  acknowledge_deferred_deploy_requests "$OVERDECK_DEPLOY_TARGET_SHA" \
    || fail deploy-queue-drain-failed "cannot acknowledge deferred deploy requests"
  DEPLOY_STATE_SHA="$prelock_sha" deploy_state finished docs-only "documentation-only landing: nothing to install"
  printf '{"stage":"deploy-local","status":"deployed-docs-only","sha":"%s","base":"%s"}\n' \
    "$prelock_sha" "${prelock_stamp:0:7}"
  exit 0
fi

# The lock is taken by a `flock --close` parent, never by an fd inside this shell: a
# descriptor opened here is inherited by pnpm/node, which outlive a killed deploy and
# hold the lock forever — a leak that wedges every later deploy.
if [[ -z "${OVERDECK_DEPLOY_LOCK_HELD:-}" ]]; then
  request_file="$QUEUE/req-$$-$RANDOM"
  printf '%s\n' "$OVERDECK_DEPLOY_TARGET_SHA" >"$request_file" \
    || fail deploy-queue-failed "cannot enqueue deploy request at $request_file"
  OVERDECK_DEPLOY_LOCK_HELD=1 OVERDECK_DEPLOY_REQUEST="$request_file" \
    flock --close -E 75 -w "$LOCK_WAIT" "$LOCK" "$BASH" "$0" "$@"
  rc=$?
  (( rc != 75 )) \
    || fail deploy-lock-timeout "could not acquire the deploy lock at $LOCK within ${LOCK_WAIT}s"
  exit "$rc"
fi

# Another deploy drained this request while we waited: its work covers ours.
if [[ -n "${OVERDECK_DEPLOY_REQUEST:-}" && ! -e "$OVERDECK_DEPLOY_REQUEST" ]]; then
  deploy_state finished coalesced "another deploy already installed this change"
  printf '{"stage":"deploy-local","status":"deployed-coalesced","request":"%s"}\n' "$OVERDECK_DEPLOY_REQUEST"
  exit 0
fi
# Let a burst coalesce while holding the serialization lock. The cap prevents a steady
# request stream from starving deployment; requests arriving after the snapshot remain.
settle_seconds="${OVERDECK_DEPLOY_SETTLE_SECONDS:-20}"
[[ "$settle_seconds" =~ ^[0-9]+$ ]] || settle_seconds=20
settle_started=$SECONDS
while (( settle_seconds > 0 && SECONDS - settle_started < 120 )); do
  newest_request=""
  shopt -s nullglob
  for request in "$QUEUE"/req-*; do
    [[ -z "$newest_request" || "$request" -nt "$newest_request" ]] && newest_request="$request"
  done
  shopt -u nullglob
  [[ -n "$newest_request" ]] || break
  request_age=$(( $(date +%s) - $(stat -c %Y "$newest_request" 2>/dev/null || printf '0') ))
  (( request_age >= settle_seconds )) && break
  sleep_for=$((settle_seconds - request_age))
  remaining=$((120 - (SECONDS - settle_started)))
  (( sleep_for > remaining )) && sleep_for=$remaining
  (( sleep_for > 0 )) || break
  sleep "$sleep_for"
done
queue_snapshot=()
shopt -s nullglob
for request in "$QUEUE"/req-*; do
  request_target=$(<"$request")
  deploy_request_is_covered "$request_target" "$OVERDECK_DEPLOY_TARGET_SHA" \
    && queue_snapshot+=("$request")
done
shopt -u nullglob

deploy_state running preparing "checking out the selected landed commit"
[[ -d "$DEPLOY/.git" ]] || fail no-deploy-clone "deploy clone missing at $DEPLOY — operator: git clone <origin-url> $DEPLOY"
# Only the serialized deploy owner may reopen the tree. Failed deploys intentionally leave
# it writable for diagnosis/retry; the end of a fully successful run closes it again.
deploy_tree_writable \
  || fail deploy-tree-unlock-failed "cannot make the deploy tree writable for deployment"
cd "$DEPLOY" || fail cd-failed "cannot cd to $DEPLOY"

# ~/.claude/bin resolves into this clone, so the checkout below swaps the live git shim.
# It refuses without its pin, so the pin is installed BEFORE the swap, not after.
PIN_INSTALLER="${DEPLOY}/modules/workstation/claude/bin/install-git-guard-real"
if [[ -f "$PIN_INSTALLER" ]]; then
  bash "$PIN_INSTALLER" >/dev/null || fail git-guard-pin-failed "install-git-guard-real could not pin a real git binary"
fi

# Deploy ONLY the exact landed target selected from the durable request. Detached checkout:
# the clone is not a dev tree, and a later origin/main movement cannot change this run.
git fetch --quiet origin main || fail fetch-failed "git fetch origin main failed"
git cat-file -e "${OVERDECK_DEPLOY_TARGET_SHA}^{commit}" 2>/dev/null \
  || fail deployment-target-missing "selected target is unavailable after fetch: $OVERDECK_DEPLOY_TARGET_SHA"
git merge-base --is-ancestor "$OVERDECK_DEPLOY_TARGET_SHA" refs/remotes/origin/main \
  || fail deployment-target-not-main "selected target is not landed on origin/main: $OVERDECK_DEPLOY_TARGET_SHA"
# The web service keeps immutable release artifacts inside its deploy checkout.
deploy_exclude="$DEPLOY/.git/info/exclude"
mkdir -p "${deploy_exclude%/*}" || fail exclude-failed "cannot create deploy clone exclude directory"
grep -qxF '/apps/web/.releases/' "$deploy_exclude" 2>/dev/null \
  || printf '/apps/web/.releases/\n' >>"$deploy_exclude" \
  || fail exclude-failed "cannot exclude generated web releases from deploy clone status"
grep -qxF '/modules/workstation/pi/agent/auth.json' "$deploy_exclude" 2>/dev/null \
  || printf '/modules/workstation/pi/agent/auth.json\n' >>"$deploy_exclude" \
  || fail exclude-failed "cannot exclude runtime Pi authentication from deploy clone status"
deploy_status=$(git status --porcelain --untracked-files=all) \
  || fail status-failed "git status failed in deploy clone"
if [[ -n "$deploy_status" ]]; then
  pi_settings_rel="modules/workstation/pi/agent/settings.json"
  pi_status=$(printf '%s\n' "$deploy_status" | grep -F " $pi_settings_rel" || true)
  if [[ $(printf '%s\n' "$deploy_status" | wc -l) -eq 1 && -n "$pi_status" ]]; then
    pi_origin=$(mktemp) || fail status-failed "cannot stage origin/main Pi settings comparison"
    git show "$OVERDECK_DEPLOY_TARGET_SHA:$pi_settings_rel" >"$pi_origin" \
      || { rm -f "$pi_origin"; fail deploy-clone-dirty "cannot read selected Pi settings from target"; }
    if cmp -s "$pi_origin" "$DEPLOY/$pi_settings_rel"; then
      deploy_status=""
    fi
    rm -f "$pi_origin"
  fi
fi
if [[ -n "$deploy_status" ]]; then
  # Owner hand-edits (docs/plans/skills/config) are wanted — land them instead
  # of failing the deploy; only unclassifiable (code-kind) dirt still blocks.
  bash "${DEPLOY}/modules/workstation/claude/bin/adopt-owner-edits" "$DEPLOY" || true
  git fetch --quiet origin main || fail fetch-failed "fetch after owner-edit adoption failed"
  deploy_status=$(git status --porcelain --untracked-files=all) \
    || fail status-failed "git status failed in deploy clone"
  if [[ -n "$deploy_status" ]]; then
    adopted_clean=1
    while IFS= read -r line; do
      p="${line:3}"; p="${p%\"}"; p="${p#\"}"
      if ! git cat-file -e "$OVERDECK_DEPLOY_TARGET_SHA:$p" 2>/dev/null \
        || ! git show "$OVERDECK_DEPLOY_TARGET_SHA:$p" | cmp -s - "$DEPLOY/$p" 2>/dev/null; then
        adopted_clean=0; break
      fi
    done < <(printf '%s\n' "$deploy_status")
    # Dirt whose every file byte-matches origin/main is landed content wearing
    # a stale index — safe to reset away, nothing is lost.
    if [[ "$adopted_clean" == 1 ]]; then
      # reset BEFORE checkout: git refuses to switch with a dirty tree, even when
      # every dirty byte matches the target, so checkout-first can never self-heal.
      git reset --hard --quiet "$OVERDECK_DEPLOY_TARGET_SHA" || fail reset-failed "post-adoption reset failed"
      git checkout --quiet --detach "$OVERDECK_DEPLOY_TARGET_SHA" || fail checkout-failed "post-adoption re-pin failed"
      deploy_status=$(git status --porcelain --untracked-files=all) || fail status-failed "git status failed in deploy clone"
    fi
  fi
fi
if [[ -n "$deploy_status" ]]; then
  fail deploy-clone-dirty "deploy clone has local changes — it must stay pristine; NEVER discard them (they are another session's work): $(printf '%s' "$deploy_status" | head -10 | tr '\n"' ' _' | cut -c1-300) — inspect $DEPLOY"
fi
# Read before the new stamp overwrites it: this is what the owner was running until now.
stamp_before=$(cat "$DEPLOY_STAMP" 2>/dev/null || true)
git checkout --quiet --detach "$OVERDECK_DEPLOY_TARGET_SHA" \
  || fail checkout-failed "checkout of selected target $OVERDECK_DEPLOY_TARGET_SHA failed"

# Capture cheap no-restart proof before any candidate can cycle a service.
declare -A pid_before=()
for svc in "${SERVICES[@]}" overdeck-kanboard.service "$ACTIONS_GATEWAY_SERVICE" reaper-notifier.service; do
  pid_before["$svc"]=$(systemctl --user show -p MainPID --value "$svc" 2>/dev/null || true)
done

# Record the exact sha THIS deploy checked out. Any concurrent `git fetch` in the clone
# (other sessions inspect it; the land queue lands every few minutes) advances
# origin/main while the build below runs for minutes, so by the time deckctl installs,
# origin/main may have moved past what is actually on disk. deckctl must verify HEAD
# against this pin, never against origin/main's live value — that comparison races.
# /usr/bin/git, not PATH `git`: the same trusted-binary boundary sync.sh already uses to
# read this file back, so the write can never be fooled by a PATH shim.
/usr/bin/git rev-parse --verify HEAD >"$DEPLOY/.git/deploy-pinned-sha.tmp" \
  && mv "$DEPLOY/.git/deploy-pinned-sha.tmp" "$DEPLOY/.git/deploy-pinned-sha" \
  || fail pin-record-failed "cannot record the checked-out deploy sha"

# The deploy that runs must be the deploy that landed. The clone itself may have started
# this process on an older revision before checking out the selected target; comparing paths
# cannot detect that case because checkout mutates the file behind the running shell. Compare
# the bytes captured at process start, then hand over exactly once to the checked-out script.
checked_out_script_digest=$(sha256sum "$DEPLOY/packaging/deploy-local.sh" | cut -d' ' -f1) \
  || fail deployment-script-identity-failed "cannot digest checked-out deploy script"
if [[ -z "${OVERDECK_DEPLOY_CHECKOUT_REEXEC:-}" \
      && "$RUNNING_DEPLOY_SCRIPT_DIGEST" != "$checked_out_script_digest" ]]; then
  export OVERDECK_DEPLOY_CHECKOUT_REEXEC=1
  exec "$BASH" "$DEPLOY/packaging/deploy-local.sh" "$@"
fi

# Docs-only short-circuit: when every path changed since the last fully-successful
# deploy is documentation, nothing installable changed — drain the queue and stop
# before touching anything. Fail closed in every direction: an absent/malformed
# stamp, an unknown base commit, a failed diff, or any single non-doc path always
# falls through to the full deploy below. To force a full deploy despite an armed
# stamp, delete it: rm "$DEPLOY/.git/harness-deployed-sha".
if stamp_sha=$(docs_only_since_stamp HEAD); then
  docs_only_sha=$(git rev-parse --short HEAD) || fail deployment-identity-failed "cannot resolve deployed commit"
  deploy_tree_readonly \
    || fail deploy-tree-lock-failed "documentation deployed but the release tree could not be made read-only"
  acknowledge_deploy_requests "${queue_snapshot[@]}" \
    || fail deploy-queue-drain-failed "cannot acknowledge completed deploy requests"
  acknowledge_deferred_deploy_requests "$OVERDECK_DEPLOY_TARGET_SHA" \
    || fail deploy-queue-drain-failed "cannot acknowledge deferred deploy requests"
  deploy_state finished docs-only "documentation-only landing: nothing to install"
  docs_landed=$(what_landed "${stamp_before:-}")
  notify_owner "Documentation landed ($docs_only_sha) — nothing to install.${docs_landed:+$'\n'$docs_landed}"
  printf '{"stage":"deploy-local","status":"deployed-docs-only","sha":"%s","base":"%s"}\n' \
    "$docs_only_sha" "${stamp_sha:0:7}"
  exit 0
fi

bash "${DEPLOY}/modules/gptbridge/install.sh" --links-only \
  || fail install-gptbridge-links-failed "gptbridge command linking failed"
"${DEPLOY}/bin/deckctl" sync apply pi || fail sync-pi-provider-failed "Pi provider model sync failed"

deploy_phase registry-preflight
verify_private_registry \
  || fail registry-unavailable "private package metadata or tarballs are not reachable through the deployment pnpm configuration"

dependencies_base=$(component_convergence_base dependencies "${stamp_before:-}" "$OVERDECK_DEPLOY_TARGET_SHA" 2>/dev/null || true)
dependency_resolution_repair=0
if [[ "$dependencies_base" =~ ^[0-9a-f]{40}$ && -d "$DEPLOY/node_modules/.pnpm" ]] \
  && (cd "$DEPLOY" && git cat-file -e "${dependencies_base}^{commit}" 2>/dev/null \
    && git diff --quiet --no-renames "$dependencies_base" HEAD -- "${PNPM_INSTALL_PATHS[@]}") \
  && ! web_build_dependencies_resolve; then
  dependency_resolution_repair=1
fi
if [[ "${OVERDECK_DEPLOY_FORCE_DEPENDENCIES:-0}" != "1" ]] \
  && pnpm_install_unchanged_since_stamp "$dependencies_base" HEAD; then
  printf 'deploy-local: dependency inputs and installation are healthy — skipping pnpm install\n' >&2
else
  deploy_phase dependencies
  deploy_state running dependencies "installing dependencies"
  if [[ "${OVERDECK_DEPLOY_FORCE_DEPENDENCIES:-0}" == "1" || "$dependency_resolution_repair" == "1" ]]; then
    run_pnpm install --frozen-lockfile --force --prefer-offline --silent \
      || fail deps-failed "clean pnpm dependency verification/install failed — see output above"
  else
    run_pnpm install --frozen-lockfile --prefer-offline --silent \
      || fail deps-failed "pnpm install failed — see output above"
  fi
  web_build_dependencies_resolve \
    || fail deps-failed "pnpm install completed but required web build dependencies are still unresolved"
fi
publish_component_receipt dependencies "$(git rev-parse HEAD)" \
  || fail component-receipt-failed "cannot record dependency convergence"
# Actions Gateway config is owner-provisioned; absent config keeps the gateway dark and
# MUST NOT fail the deploy of everything else. Its immutable artifact and unit still stage
# so enabling later cannot accidentally execute mutable checkout output.
gateway_enabled=1
if [[ ! -r "$ACTIONS_GATEWAY_CONFIG" ]]; then
  gateway_enabled=0
  printf '{"stage":"deploy-local","status":"actions-gateway-dark","detail":"owner config missing at %s; immutable release will stage but activation stays disabled"}\n' "$ACTIONS_GATEWAY_CONFIG" >&2
fi

RELEASES_DIR="${OVERDECK_WEB_RELEASES_DIR:-${DEPLOY}/apps/web/.releases}"
discard_release() {
  local target="$1"
  [[ ! -e "$target" ]] || {
    chmod -R u+w "$target" && rm -rf "$target"
  }
}
freeze_dependencies() {
  local target="$1" immutable="$1/node_modules.immutable" self_link
  self_link="$target/node_modules/.pnpm/node_modules/@overdeck/web"
  if [[ -L "$self_link" ]]; then
    rm "$self_link" && ln -s ../../../.. "$self_link" || return 1
  fi
  cp -a --reflink=auto "$target/node_modules" "$immutable" || return 1
  rm -rf "$target/node_modules" || return 1
  mv "$immutable" "$target/node_modules" || return 1
  chmod -R a-w "$target/node_modules" || return 1
  bash "${DEPLOY}/packaging/web-deps-verify.sh" "$target"
}
mkdir -p "$RELEASES_DIR" || fail release-dir-failed "cannot create $RELEASES_DIR"

# Skip the web rebuild entirely when nothing that could affect apps/web's output has
# changed since the release currently being served. Release dirs are named
# .build-<full-sha-at-build-time>-XXXXXX (see mktemp calls below), so the served
# release's own directory name IS the last-deployed commit — no separate state file
# to keep in sync. Absent/unrecognized/unreachable prior commit -> always rebuild
# (fail closed: an inconclusive diff is never a reason to skip).
WEB_BUILD_NEEDED=1
prev_sha=""
candidate_release=""
if [[ -L "$RELEASES_DIR/current" ]]; then
  candidate_release="$(readlink -f "$RELEASES_DIR/current")"
  prev_base="$(basename "$candidate_release")"
  case "$prev_base" in
    .build-*-??????) prev_sha="${prev_base#.build-}"; prev_sha="${prev_sha%-??????}" ;;
  esac
fi
# readlink -f resolves a dangling symlink without error, so the target must be checked
# to actually exist and look like a built release before the skip is allowed to fire —
# otherwise a pruned/removed "current" would silently hand a phantom path downstream.
if [[ -n "$prev_sha" && -d "$candidate_release" && -f "$candidate_release/server/entry.mjs" ]] \
  && git cat-file -e "${prev_sha}^{commit}" 2>/dev/null; then
  web_changed=$(git diff --name-only "$prev_sha" HEAD -- \
    apps/web packages package.json pnpm-lock.yaml pnpm-workspace.yaml '*.npmrc' 'tsconfig*.json') \
    || web_changed="unknown"
  [[ "$web_changed" == "unknown" || -n "$web_changed" ]] || WEB_BUILD_NEEDED=0
fi

if [[ "$WEB_BUILD_NEEDED" == 0 ]]; then
  printf 'deploy-local: apps/web unchanged since %s — reusing the current release, skipping build\n' "$prev_sha" >&2
  release="$candidate_release"
else
release="$(mktemp -d "${RELEASES_DIR}/.build-$(git rev-parse HEAD)-XXXXXX")" \
  || fail release-dir-failed "cannot safely create staged release"
deps_stage="$(mktemp -d "${RELEASES_DIR}/.deps-$(git rev-parse HEAD)-XXXXXX")" \
  || { rm -rf "$release"; fail release-dir-failed "cannot safely create dependency snapshot"; }
if [[ ! -L "$RELEASES_DIR/current" && -f "${DEPLOY}/apps/web/dist/server/entry.mjs" ]]; then
  bootstrap="$(mktemp -d "${RELEASES_DIR}/.bootstrap-$(git rev-parse HEAD)-XXXXXX")" \
    || fail release-bootstrap-failed "cannot safely create bootstrap release"
  cp -a "${DEPLOY}/apps/web/dist/." "$bootstrap/" \
    || fail release-bootstrap-failed "cannot preserve the currently served release"
  run_pnpm --filter web deploy --prod --legacy --prefer-offline "$deps_stage" >/tmp/overdeck-deploy-deps.log 2>&1 \
    || fail release-bootstrap-failed "cannot snapshot runtime dependencies"
  mv "$deps_stage/node_modules" "$bootstrap/node_modules" \
    || fail release-bootstrap-failed "cannot attach bootstrap dependencies"
  rm -rf "$deps_stage"
  deps_stage="$(mktemp -d "${RELEASES_DIR}/.deps-$(git rev-parse HEAD)-XXXXXX")" \
    || fail release-dir-failed "cannot safely create dependency snapshot"
  freeze_dependencies "$bootstrap" \
    || fail release-bootstrap-failed "cannot make bootstrap dependencies immutable"
  bash "${DEPLOY}/packaging/web-release.sh" promote "$bootstrap" \
    || fail release-bootstrap-failed "cannot record the currently served release"
fi

deploy_phase web-build
deploy_state running building-web "building the web app"
run_pnpm --filter web build >/tmp/overdeck-deploy-build.log 2>&1 \
  || { rm -rf "$release" "$deps_stage"; fail build-failed "web build failed — tail: $(tail -c 300 /tmp/overdeck-deploy-build.log | tr '\n\"' ' _')"; }
cp -a --reflink=auto "${DEPLOY}/apps/web/dist/." "$release/" \
  || { rm -rf "$release" "$deps_stage"; fail build-stage-failed "cannot stage the built web release"; }
run_pnpm --filter web deploy --prod --legacy --prefer-offline "$deps_stage" >/tmp/overdeck-deploy-deps.log 2>&1 \
  || { rm -rf "$release" "$deps_stage"; fail deps-snapshot-failed "runtime dependency snapshot failed — tail: $(tail -c 300 /tmp/overdeck-deploy-deps.log | tr '\n\"' ' _')"; }
mv "$deps_stage/node_modules" "$release/node_modules" \
  || { rm -rf "$release" "$deps_stage"; fail deps-snapshot-failed "cannot attach runtime dependency snapshot"; }
rm -rf "$deps_stage"
if ! freeze_dependencies "$release"; then
  discard_release "$release" || fail release-cleanup-failed "cannot remove a rejected dependency snapshot"
  fail deps-snapshot-failed "runtime dependency snapshot is not immutable and contained"
fi

if ! OVERDECK_WEB_PREFLIGHT_LOG=/tmp/overdeck-web-preflight.log \
  OVERDECK_COLLECTOR_URL="$COLLECTOR_URL" \
  bash "${DEPLOY}/packaging/web-preflight.sh" "$release" "${DEPLOY}/apps/web/src/pages"; then
  discard_release "$release" || fail release-cleanup-failed "cannot remove a rejected web release"
  fail web-preflight-failed "staged release failed its isolated route sweep"
fi
fi

deploy_state running installing "installing commands, services and units"
"${DEPLOY}/bin/deckctl" sync apply bin || fail install-user-bin-failed "deckctl sync apply bin failed"
"${DEPLOY}/bin/deckctl" sync apply claude bin || fail install-workstation-bin-failed "deckctl sync apply claude bin failed"
"${DEPLOY}/bin/deckctl" sync apply claude lib || fail install-workstation-lib-failed "deckctl sync apply claude lib failed"
# Pin BEFORE anything below can call systemctl/git: bin/ just went live, so
# ~/.claude/bin/systemctl and ~/.claude/bin/git are shims from this line on, first on
# PATH, and unpinned shims refuse closed (see install-tool-shims-real/install-git-guard-real).
bash "${DEPLOY}/modules/workstation/claude/bin/install-git-guard-real" >/dev/null \
  || fail git-guard-pin-failed "install-git-guard-real could not pin a real git binary"
bash "${DEPLOY}/modules/workstation/claude/bin/install-tool-shims-real" >/dev/null
tool_shim_pin_rc=$?
if [[ $tool_shim_pin_rc -ne 0 && $tool_shim_pin_rc -ne 1 ]]; then
  fail tool-shim-pin-failed "install-tool-shims-real failed unexpectedly (rc=$tool_shim_pin_rc)"
fi
python3 "${DEPLOY}/modules/workstation/claude/bin/generate-tool-shims" --check >/dev/null \
  || fail tool-shims-stale "committed PATH shims (bin/ccr, bin/podman, bin/systemctl) do not match tools.json — run modules/workstation/claude/bin/generate-tool-shims and commit the result"
"${DEPLOY}/bin/deckctl" sync apply claude cloudflare-token-targets.json || fail install-cloudflare-token-registry-failed "deckctl sync apply claude cloudflare-token-targets.json failed"
"${DEPLOY}/bin/deckctl" sync apply claude buildbox-hosts.json || fail install-buildbox-registry-failed "deckctl sync apply claude buildbox-hosts.json failed"
install -Dm644 "${DEPLOY}/modules/monitor/systemd/user/agent.slice" "$HOME/.config/systemd/user/agent.slice" \
  || fail install-agent-slice-failed "cannot install agent.slice"
install -Dm644 "${DEPLOY}/packaging/overdeck-deploy.path" "${DEPLOY}/packaging/overdeck-deploy.service" "$HOME/.config/systemd/user/" \
  || fail install-deploy-queue-failed "cannot install deploy queue units"
install -Dm644 "${DEPLOY}/modules/fire-consumer/overdeck-fire-consumer.path" "${DEPLOY}/modules/fire-consumer/overdeck-fire-consumer.service" "$HOME/.config/systemd/user/" \
  || fail install-fire-consumer-failed "cannot install fire consumer units"
install -Dm644 "${DEPLOY}/modules/workstation/systemd/user/factory-k3s-cleanup.service" "${DEPLOY}/modules/workstation/systemd/user/factory-k3s-cleanup.timer" "$HOME/.config/systemd/user/" \
  || fail install-factory-k3s-cleanup-failed "cannot install Factory k3s cleanup units"
install -Dm644 "${DEPLOY}/modules/workstation/systemd/user/session-transcript-converge.service" "${DEPLOY}/modules/workstation/systemd/user/session-transcript-converge.timer" "$HOME/.config/systemd/user/" \
  || fail install-session-transcript-converge-failed "cannot install session transcript converge units"
install -Dm644 "${DEPLOY}/modules/workstation/systemd/user/request-evidence-drain.path" "${DEPLOY}/modules/workstation/systemd/user/request-evidence-drain.service" "${DEPLOY}/modules/workstation/systemd/user/request-evidence-drain.timer" "$HOME/.config/systemd/user/" \
  || fail install-request-evidence-drain-failed "cannot install request evidence drain units"
install -Dm644 "${DEPLOY}/modules/systray/systemd/systray-gateway-grant-renewal.service" "${DEPLOY}/modules/systray/systemd/systray-gateway-grant-renewal.timer" "$HOME/.config/systemd/user/" \
  || fail install-gateway-grant-renewal-failed "cannot install Gateway grant renewal units"
install -d -m 700 "$HOME/Projects/.overdeck-runtime" \
  || fail create-gateway-grant-renewal-runtime-failed "cannot create Gateway grant renewal runtime directory"
systemctl --user daemon-reload || fail units-reload-failed "systemd user manager cannot reload installed units"
systemctl --user enable --now factory-k3s-cleanup.timer \
  || fail enable-factory-k3s-cleanup-failed "cannot enable factory-k3s-cleanup.timer"
systemctl --user enable --now session-transcript-converge.timer \
  || fail enable-session-transcript-converge-failed "cannot enable session-transcript-converge.timer"
systemctl --user enable --now request-evidence-drain.path request-evidence-drain.timer \
  || fail enable-request-evidence-drain-failed "cannot enable request evidence drainer"
systemctl --user enable --now systray-gateway-grant-renewal.timer \
  || fail enable-gateway-grant-renewal-failed "cannot enable Gateway grant renewal timer"
# Recover an already-expired route immediately; the timer maintains it afterward. An
# individual account being unavailable must not roll back an otherwise healthy deploy.
if ! systemctl --user start systray-gateway-grant-renewal.service; then
  printf 'deploy-local: Gateway grant renewal reported an account failure; timer will retry\n' >&2
fi
systemctl --user enable --now overdeck-deploy.path \
  || fail enable-deploy-queue-failed "cannot enable overdeck-deploy.path"
systemctl --user enable --now overdeck-fire-consumer.path \
  || fail enable-fire-consumer-failed "cannot enable overdeck-fire-consumer.path"
python3 "${DEPLOY}/modules/systray/install.py" \
  || fail install-systray-commands-failed "systray command installation failed"
bash "${DEPLOY}/modules/workstation/claude/bin/install-git-guard-real" >/dev/null \
  || fail git-guard-pin-failed "install-git-guard-real could not pin a real git binary"
# rc 3 = only non-live checkout copies drift: a property of somebody's working tree, not
# of what was deployed, so it is printed and the deploy continues. rc 1/2 still block.
bash "${DEPLOY}/modules/workstation/claude/bin/shim-drift-check" >/dev/null
drift_rc=$?
case "$drift_rc" in
  0 | 3) ;;
  *) fail shim-drift "the PATH shims this machine executes are not the landed ones (rc=$drift_rc) — run ${DEPLOY}/modules/workstation/claude/bin/shim-drift-check" ;;
esac
# The pre-push land guard and the lander that satisfies it must move together, so the guard is
# re-armed from the same landed commit that supplied the lander.
bash "${DEPLOY}/modules/workstation/claude/workflows/hooks/install-land-guard.sh" "${OVERDECK_DEV_ROOT:-$HOME/Projects/overdeck}" >/dev/null \
  || fail land-guard-failed "could not install the guarded-trunk pre-push hook"
deployment_sha=$(git rev-parse HEAD) || fail deployment-identity-failed "cannot resolve deployed commit"
collect_delivery_request_ids "${stamp_before:-}" "$deployment_sha" \
  || fail delivery-manifest-invalid "request-bearing delivery manifest is invalid"
DELIVERY_DEPLOYMENT_ID="local:${deployment_sha}:${DEPLOY_STARTED_AT}"
emit_deploy_evidence deploy-started --event-key "deploy:${deployment_sha}:${DEPLOY_STARTED_AT}:started" \
  --deployment-id "$DELIVERY_DEPLOYMENT_ID" --release "$deployment_sha" \
  || fail delivery-evidence-start-failed "exact request deployment start could not be recorded"
# Capture the controller unit before its installer overwrites the target. Its stable
# current path makes unit drift independent from the immutable artifact identity.
unit_dir="$HOME/.config/systemd/user"
bun_bin=$(command -v bun || true)
controller_rendered=$(sed -e "s#__CONTROLLER_CURRENT__#${BACKEND_RELEASE_ROOT}/controller/current#g" \
  -e "s#__BUN_BIN__#${bun_bin}#g" "${DEPLOY}/packaging/overdeck-controller.service") || controller_rendered=""
controller_unit_changed=1
printf '%s\n' "$controller_rendered" | cmp -s - "$unit_dir/overdeck-controller.service" \
  && controller_unit_changed=0

collector_base=$(component_convergence_base collector "${stamp_before:-}" "$deployment_sha" 2>/dev/null || true)
collector_current="$BACKEND_RELEASE_ROOT/collector/current"
collector_current_valid=0
if [[ -L "$collector_current" ]]; then
  collector_current_value=$(readlink "$collector_current" 2>/dev/null || true)
  if [[ "$collector_current_value" =~ ^releases/([0-9a-f]{40})$ ]]; then
    collector_current_sha="${BASH_REMATCH[1]}"
    OVERDECK_BACKEND_RELEASE_ROOT="$BACKEND_RELEASE_ROOT" \
      bash "${DEPLOY}/packaging/backend-release.sh" validate collector "$collector_current_sha" \
      && collector_current_valid=1
  fi
fi
collector_work_needed=1
if (( collector_current_valid )) && collector_unchanged_since_stamp "$collector_base" "$deployment_sha"; then
  collector_work_needed=0
else
  deploy_state running staging-collector "building immutable collector release $deployment_sha"
  if [[ ! -e "$collector_current" && ! -L "$collector_current" ]]; then
    bootstrap_sha="$collector_base"
    if ! systemctl --user is-active --quiet overdeck-collector.service; then
      bootstrap_sha="$deployment_sha"
    fi
    [[ "$bootstrap_sha" =~ ^[0-9a-f]{40}$ ]] \
      || fail collector-bootstrap-failed "active collector has no exact prior revision for rollback"
    OVERDECK_BACKEND_RELEASE_ROOT="$BACKEND_RELEASE_ROOT" \
      LOCAL_GATE_ACTIVE=1 bash "${DEPLOY}/packaging/stage-backend-release.sh" collector "$bootstrap_sha" >/dev/null \
      || fail collector-bootstrap-failed "cannot stage prior collector release $bootstrap_sha"
    OVERDECK_BACKEND_RELEASE_ROOT="$BACKEND_RELEASE_ROOT" \
      bash "${DEPLOY}/packaging/backend-release.sh" seed collector "$bootstrap_sha" \
      || fail collector-bootstrap-failed "cannot establish prior collector release $bootstrap_sha"
  fi
  OVERDECK_BACKEND_RELEASE_ROOT="$BACKEND_RELEASE_ROOT" \
    LOCAL_GATE_ACTIVE=1 bash "${DEPLOY}/packaging/stage-backend-release.sh" collector "$deployment_sha" >/dev/null \
    || fail collector-stage-failed "cannot stage collector release $deployment_sha"
fi

controller_base=$(component_convergence_base controller "${stamp_before:-}" "$deployment_sha" 2>/dev/null || true)
controller_current="$BACKEND_RELEASE_ROOT/controller/current"
controller_current_valid=0
if [[ -L "$controller_current" ]]; then
  controller_current_value=$(readlink "$controller_current" 2>/dev/null || true)
  if [[ "$controller_current_value" =~ ^releases/([0-9a-f]{40})$ ]]; then
    controller_current_sha="${BASH_REMATCH[1]}"
    OVERDECK_BACKEND_RELEASE_ROOT="$BACKEND_RELEASE_ROOT" \
      bash "${DEPLOY}/packaging/backend-release.sh" validate controller "$controller_current_sha" \
      && controller_current_valid=1
  fi
fi
controller_work_needed=1
if (( controller_current_valid && ! controller_unit_changed )) \
  && controller_unchanged_since_stamp "$controller_base" "$deployment_sha"; then
  controller_work_needed=0
else
  deploy_state running staging-controller "building immutable controller release $deployment_sha"
  if [[ ! -e "$controller_current" && ! -L "$controller_current" ]]; then
    controller_bootstrap_sha="$controller_base"
    if ! systemctl --user is-active --quiet overdeck-controller.service; then
      controller_bootstrap_sha="$deployment_sha"
    fi
    [[ "$controller_bootstrap_sha" =~ ^[0-9a-f]{40}$ ]] \
      || fail controller-bootstrap-failed "active controller has no exact prior revision for rollback"
    OVERDECK_BACKEND_RELEASE_ROOT="$BACKEND_RELEASE_ROOT" \
      LOCAL_GATE_ACTIVE=1 bash "${DEPLOY}/packaging/stage-backend-release.sh" controller "$controller_bootstrap_sha" >/dev/null \
      || fail controller-bootstrap-failed "cannot stage prior controller release $controller_bootstrap_sha"
    OVERDECK_BACKEND_RELEASE_ROOT="$BACKEND_RELEASE_ROOT" \
      bash "${DEPLOY}/packaging/backend-release.sh" seed controller "$controller_bootstrap_sha" \
      || fail controller-bootstrap-failed "cannot establish prior controller release $controller_bootstrap_sha"
  fi
  OVERDECK_BACKEND_RELEASE_ROOT="$BACKEND_RELEASE_ROOT" \
    LOCAL_GATE_ACTIVE=1 bash "${DEPLOY}/packaging/stage-backend-release.sh" controller "$deployment_sha" >/dev/null \
    || fail controller-stage-failed "cannot stage controller release $deployment_sha"
fi

botmaster_base=$(component_convergence_base botmaster-proxy "${stamp_before:-}" "$deployment_sha" 2>/dev/null || true)
botmaster_current="$BACKEND_RELEASE_ROOT/botmaster-proxy/current"
proxy_rendered=$(sed -e "s#__BOTMASTER_PROXY_CURRENT__#${botmaster_current}#g" \
  -e "s#__BUN_BIN__#${bun_bin}#g" "${DEPLOY}/packaging/botmaster-proxy.service") || proxy_rendered=""
proxy_unit_changed=1
printf '%s\n' "$proxy_rendered" | cmp -s - "$unit_dir/botmaster-proxy.service" && proxy_unit_changed=0
botmaster_current_valid=0
if [[ -L "$botmaster_current" ]]; then
  botmaster_current_value=$(readlink "$botmaster_current" 2>/dev/null || true)
  if [[ "$botmaster_current_value" =~ ^releases/([0-9a-f]{40})$ ]]; then
    botmaster_current_sha="${BASH_REMATCH[1]}"
    OVERDECK_BACKEND_RELEASE_ROOT="$BACKEND_RELEASE_ROOT" \
      bash "${DEPLOY}/packaging/backend-release.sh" validate botmaster-proxy "$botmaster_current_sha" \
      && botmaster_current_valid=1
  fi
fi
botmaster_work_needed=1
if (( botmaster_current_valid && ! proxy_unit_changed )) \
  && service_unchanged_since_stamp botmaster-proxy.service "$botmaster_base" "$deployment_sha" "${BOTMASTER_PROXY_RESTART_PATHS[@]}"; then
  botmaster_work_needed=0
else
  deploy_state running staging-botmaster-proxy "building immutable Botmaster proxy release $deployment_sha"
  OVERDECK_BACKEND_RELEASE_ROOT="$BACKEND_RELEASE_ROOT" \
    bash "${DEPLOY}/packaging/stage-backend-release.sh" botmaster-proxy "$deployment_sha" >/dev/null \
    || fail botmaster-proxy-stage-failed "cannot stage Botmaster proxy release $deployment_sha"
  if [[ ! -e "$botmaster_current" && ! -L "$botmaster_current" ]]; then
    botmaster_bootstrap_sha="$botmaster_base"
    if ! systemctl --user is-active --quiet botmaster-proxy.service; then
      botmaster_bootstrap_sha="$deployment_sha"
    fi
    [[ "$botmaster_bootstrap_sha" =~ ^[0-9a-f]{40}$ ]] \
      || fail botmaster-proxy-bootstrap-failed "active Botmaster proxy has no exact prior revision for rollback"
    if [[ "$botmaster_bootstrap_sha" != "$deployment_sha" ]]; then
      OVERDECK_BACKEND_RELEASE_ROOT="$BACKEND_RELEASE_ROOT" \
        bash "${DEPLOY}/packaging/stage-backend-release.sh" botmaster-proxy "$botmaster_bootstrap_sha" >/dev/null \
        || fail botmaster-proxy-bootstrap-failed "cannot stage prior Botmaster proxy release $botmaster_bootstrap_sha"
    fi
    OVERDECK_BACKEND_RELEASE_ROOT="$BACKEND_RELEASE_ROOT" \
      bash "${DEPLOY}/packaging/backend-release.sh" seed botmaster-proxy "$botmaster_bootstrap_sha" \
      || fail botmaster-proxy-bootstrap-failed "cannot establish prior Botmaster proxy release $botmaster_bootstrap_sha"
  fi
fi

gateway_base=$(component_convergence_base actions-gateway "${stamp_before:-}" "$deployment_sha" 2>/dev/null || true)
gateway_current="$BACKEND_RELEASE_ROOT/actions-gateway/current"
node_bin=$(command -v node) || fail actions-gateway-node-missing "node is required for Actions Gateway"
gateway_rendered=$(sed -e "s#__ACTIONS_GATEWAY_CURRENT__#${gateway_current}#g" \
  -e "s#__NODE_BIN__#${node_bin}#g" "${DEPLOY}/packaging/actions-gateway.service") || gateway_rendered=""
gateway_unit_changed=1
printf '%s\n' "$gateway_rendered" | cmp -s - "$unit_dir/$ACTIONS_GATEWAY_SERVICE" && gateway_unit_changed=0
gateway_config_state="${XDG_STATE_HOME:-$HOME/.local/state}/overdeck/actions-gateway.config.sha256"
gateway_config_hash=""
gateway_stored_hash=""
if (( gateway_enabled )); then
  gateway_config_hash=$(sha256sum "$ACTIONS_GATEWAY_CONFIG" 2>/dev/null | cut -d' ' -f1) || gateway_config_hash=""
  gateway_stored_hash=$(cat "$gateway_config_state" 2>/dev/null || true)
fi
gateway_current_valid=0
if [[ -L "$gateway_current" ]]; then
  gateway_current_value=$(readlink "$gateway_current" 2>/dev/null || true)
  if [[ "$gateway_current_value" =~ ^releases/([0-9a-f]{40})$ ]]; then
    gateway_current_sha="${BASH_REMATCH[1]}"
    OVERDECK_BACKEND_RELEASE_ROOT="$BACKEND_RELEASE_ROOT" \
      bash "${DEPLOY}/packaging/backend-release.sh" validate actions-gateway "$gateway_current_sha" \
      && gateway_current_valid=1
  fi
fi
gateway_work_needed=1
if (( gateway_current_valid && ! gateway_unit_changed && gateway_enabled )) \
  && [[ "$gateway_config_hash" =~ ^[0-9a-f]{64}$ && "$gateway_stored_hash" == "$gateway_config_hash" ]] \
  && service_unchanged_since_stamp "$ACTIONS_GATEWAY_SERVICE" "$gateway_base" "$deployment_sha" "${ACTIONS_GATEWAY_RESTART_PATHS[@]}"; then
  gateway_work_needed=0
elif (( gateway_current_valid && ! gateway_unit_changed && ! gateway_enabled )) \
  && git -C "$DEPLOY" diff --quiet --no-renames "$gateway_base" "$deployment_sha" -- "${ACTIONS_GATEWAY_RESTART_PATHS[@]}" 2>/dev/null; then
  gateway_work_needed=0
else
  deploy_state running staging-actions-gateway "building immutable Actions Gateway release $deployment_sha"
  OVERDECK_BACKEND_RELEASE_ROOT="$BACKEND_RELEASE_ROOT" \
    bash "${DEPLOY}/packaging/stage-backend-release.sh" actions-gateway "$deployment_sha" >/dev/null \
    || fail actions-gateway-stage-failed "cannot stage Actions Gateway release $deployment_sha"
  if [[ ! -e "$gateway_current" && ! -L "$gateway_current" ]]; then
    gateway_bootstrap_sha="$deployment_sha"
    if (( gateway_enabled )) && systemctl --user is-active --quiet "$ACTIONS_GATEWAY_SERVICE"; then
      gateway_bootstrap_sha="$gateway_base"
    fi
    [[ "$gateway_bootstrap_sha" =~ ^[0-9a-f]{40}$ ]] \
      || fail actions-gateway-bootstrap-failed "active Actions Gateway has no exact prior revision for rollback"
    if [[ "$gateway_bootstrap_sha" != "$deployment_sha" ]]; then
      OVERDECK_BACKEND_RELEASE_ROOT="$BACKEND_RELEASE_ROOT" \
        bash "${DEPLOY}/packaging/stage-backend-release.sh" actions-gateway "$gateway_bootstrap_sha" >/dev/null \
        || fail actions-gateway-bootstrap-failed "cannot stage prior Actions Gateway release $gateway_bootstrap_sha"
    fi
    OVERDECK_BACKEND_RELEASE_ROOT="$BACKEND_RELEASE_ROOT" \
      bash "${DEPLOY}/packaging/backend-release.sh" seed actions-gateway "$gateway_bootstrap_sha" \
      || fail actions-gateway-bootstrap-failed "cannot establish prior Actions Gateway release $gateway_bootstrap_sha"
  fi
fi
if (( ! gateway_enabled && gateway_work_needed )); then
  keep_service_dark "$ACTIONS_GATEWAY_SERVICE" \
    || fail actions-gateway-dark-failed "owner config is absent but Actions Gateway could not be stopped and disabled"
  OVERDECK_BACKEND_RELEASE_ROOT="$BACKEND_RELEASE_ROOT" \
    bash "${DEPLOY}/packaging/backend-release.sh" select-inactive actions-gateway "$deployment_sha" "$ACTIONS_GATEWAY_SERVICE" \
    || fail actions-gateway-dark-select-failed "cannot select the staged Actions Gateway release while it is dark"
fi

# Remaining rendered-unit decisions must be captured before their installers overwrite targets.
web_node_bin=/usr/bin/node
[[ -x "$web_node_bin" ]] || web_node_bin=$(command -v node || true)
web_rendered=$(sed -e "s#__WEB_DIR__#${DEPLOY}/apps/web#g" \
  -e "s#__WEB_RELEASE_CURRENT__#${DEPLOY}/apps/web/.releases/current#g" \
  -e "s#__NODE_BIN__#${web_node_bin}#g" "${DEPLOY}/packaging/overdeck-web.service") || web_rendered=""
watchdog_rendered=$(sed -e "s#__WATCHDOG_BIN__#${HOME}/.local/bin/overdeck-web-watchdog#g" \
  -e "s#__PAGES_DIR__#${DEPLOY}/apps/web/src/pages#g" "${DEPLOY}/packaging/overdeck-web-watchdog.service") || watchdog_rendered=""
web_unit_changed=1; printf '%s\n' "$web_rendered" | cmp -s - "$unit_dir/overdeck-web.service" && web_unit_changed=0
watchdog_unit_changed=1; printf '%s\n' "$watchdog_rendered" | cmp -s - "$unit_dir/overdeck-web-watchdog.service" && watchdog_unit_changed=0

kanboard_status=$(bash "${DEPLOY}/packaging/install-kanboard.sh") \
  || { printf '%s\n' "$kanboard_status"; fail install-kanboard-failed "install-kanboard.sh failed"; }
printf '%s\n' "$kanboard_status"
publish_component_receipt kanboard "$deployment_sha" \
  || fail component-receipt-failed "cannot record Kanboard convergence"
bash "${DEPLOY}/packaging/install-controller.sh" || fail install-controller-failed "install-controller.sh failed"
bash "${DEPLOY}/packaging/install.sh" || fail install-collector-failed "install.sh failed"
bash "${DEPLOY}/packaging/install-botmaster-proxy.sh" || fail install-botmaster-proxy-failed "install-botmaster-proxy.sh failed"
bash "${DEPLOY}/packaging/install-actions-gateway.sh" || fail install-actions-gateway-failed "install-actions-gateway.sh failed"
if (( ! gateway_enabled )); then
  keep_service_dark "$ACTIONS_GATEWAY_SERVICE" \
    || fail actions-gateway-dark-failed "owner config is absent but installed Actions Gateway could not be stopped and disabled"
fi
if (( collector_work_needed )); then
  deploy_state running activating-collector "activating collector release $deployment_sha"
  systemctl --user start overdeck-collector-startup-boost.service \
    || fail collector-startup-boost-failed "cannot lift the collector CPU quota for its bounded activation window"
  collector_activation_started_ms=$(date +%s%3N)
  collector_activation_output=$(OVERDECK_BACKEND_RELEASE_ROOT="$BACKEND_RELEASE_ROOT" \
    OVERDECK_BACKEND_READINESS_TIMEOUT="$SMOKE_READINESS_TIMEOUT" \
    OVERDECK_BACKEND_READINESS_DELAY="$SMOKE_READINESS_DELAY" \
    bash "${DEPLOY}/packaging/backend-release.sh" activate collector "$deployment_sha" \
      overdeck-collector.service "${DEPLOY}/packaging/backend-readiness.sh" \
      collector "$COLLECTOR_BACKEND_URL/health" "$COLLECTOR_TOKEN_FILE" 2>&1)
  collector_activation_rc=$?
  collector_activation_finished_ms=$(date +%s%3N)
  collector_boost_stop_output=$(systemctl --user stop overdeck-collector-startup-boost.service 2>&1)
  collector_boost_stop_rc=$?
  printf '%s\n' "$collector_activation_output"
  if (( collector_boost_stop_rc != 0 )); then
    fail collector-startup-boost-restore-failed \
      "collector activation finished but its steady 25% CPU quota was not restored: $(printf '%s' "$collector_boost_stop_output" | tail -c 300 | tr '\n\"' ' _')"
  fi
  printf 'deploy-local: collector exact readiness in %dms; steady CPU quota restored\n' \
    "$((collector_activation_finished_ms - collector_activation_started_ms))"
  case "$collector_activation_rc" in
    0) ;;
    20) fail collector-candidate-failed-rolled-back "candidate collector failed exact-SHA readiness; prior immutable release was restored" ;;
    21) fail collector-rollback-failed "candidate collector failed and prior release did not recover" ;;
    22) fail collector-activation-lock-timeout "collector activation lock timed out" transient ;;
    *) fail collector-activation-failed "collector activation failed (rc=$collector_activation_rc): $(printf '%s' "$collector_activation_output" | tail -c 300 | tr '\n\"' ' _')" ;;
  esac
fi
publish_component_receipt collector "$deployment_sha" \
  || fail component-receipt-failed "cannot record collector convergence"
if (( controller_work_needed )); then
  deploy_state running activating-controller "activating controller release $deployment_sha"
  controller_activation_output=$(OVERDECK_BACKEND_RELEASE_ROOT="$BACKEND_RELEASE_ROOT" \
    OVERDECK_BACKEND_READINESS_TIMEOUT="$SMOKE_READINESS_TIMEOUT" \
    OVERDECK_BACKEND_READINESS_DELAY="$SMOKE_READINESS_DELAY" \
    bash "${DEPLOY}/packaging/backend-release.sh" activate controller "$deployment_sha" \
      overdeck-controller.service "${DEPLOY}/packaging/backend-readiness.sh" \
      controller "$CONTROLLER_BACKEND_URL/health" "$CONTROLLER_TOKEN_FILE" 2>&1)
  controller_activation_rc=$?
  printf '%s\n' "$controller_activation_output"
  case "$controller_activation_rc" in
    0) ;;
    20) fail controller-candidate-failed-rolled-back "candidate controller failed exact-SHA readiness; prior immutable release was restored" ;;
    21) fail controller-rollback-failed "candidate controller failed and prior release did not recover" ;;
    22) fail controller-activation-lock-timeout "controller activation lock timed out" transient ;;
    *) fail controller-activation-failed "controller activation failed (rc=$controller_activation_rc): $(printf '%s' "$controller_activation_output" | tail -c 300 | tr '\n\"' ' _')" ;;
  esac
fi
publish_component_receipt controller "$deployment_sha" \
  || fail component-receipt-failed "cannot record controller convergence"
if (( botmaster_work_needed )); then
  deploy_state running activating-botmaster-proxy "activating Botmaster proxy release $deployment_sha"
  botmaster_activation_output=$(OVERDECK_BACKEND_RELEASE_ROOT="$BACKEND_RELEASE_ROOT" \
    OVERDECK_BACKEND_READINESS_TIMEOUT="$SMOKE_READINESS_TIMEOUT" \
    OVERDECK_BACKEND_READINESS_DELAY="$SMOKE_READINESS_DELAY" \
    bash "${DEPLOY}/packaging/backend-release.sh" activate botmaster-proxy "$deployment_sha" \
      botmaster-proxy.service "${DEPLOY}/packaging/backend-readiness.sh" \
      botmaster-proxy "$BOTMASTER_PROXY_URL/health" - 2>&1)
  botmaster_activation_rc=$?
  printf '%s\n' "$botmaster_activation_output"
  case "$botmaster_activation_rc" in
    0) ;;
    20) fail botmaster-proxy-candidate-failed-rolled-back "candidate Botmaster proxy failed exact-SHA readiness; prior immutable release was restored" ;;
    21) fail botmaster-proxy-rollback-failed "candidate Botmaster proxy failed and prior release did not recover" ;;
    22) fail botmaster-proxy-activation-lock-timeout "Botmaster proxy activation lock timed out" transient ;;
    *) fail botmaster-proxy-activation-failed "Botmaster proxy activation failed (rc=$botmaster_activation_rc): $(printf '%s' "$botmaster_activation_output" | tail -c 300 | tr '\n\"' ' _')" ;;
  esac
fi
publish_component_receipt botmaster-proxy "$deployment_sha" \
  || fail component-receipt-failed "cannot record Botmaster proxy convergence"
if (( gateway_enabled )); then
  if (( gateway_work_needed )); then
    deploy_state running activating-actions-gateway "activating Actions Gateway release $deployment_sha"
    gateway_activation_output=$(OVERDECK_BACKEND_RELEASE_ROOT="$BACKEND_RELEASE_ROOT" \
      OVERDECK_BACKEND_READINESS_TIMEOUT="$SMOKE_READINESS_TIMEOUT" \
      OVERDECK_BACKEND_READINESS_DELAY="$SMOKE_READINESS_DELAY" \
      bash "${DEPLOY}/packaging/backend-release.sh" activate actions-gateway "$deployment_sha" \
        "$ACTIONS_GATEWAY_SERVICE" "${DEPLOY}/packaging/actions-gateway-readiness.sh" \
        "$ACTIONS_GATEWAY_URL" "$ACTIONS_GATEWAY_CONFIG" "$node_bin" 2>&1)
    gateway_activation_rc=$?
    printf '%s\n' "$gateway_activation_output"
    case "$gateway_activation_rc" in
      0) ;;
      20) fail actions-gateway-candidate-failed-rolled-back "candidate Actions Gateway failed exact-SHA authenticated readiness; prior immutable release was restored" ;;
      21) fail actions-gateway-rollback-failed "candidate Actions Gateway failed and prior release did not recover" ;;
      22) fail actions-gateway-activation-lock-timeout "Actions Gateway activation lock timed out" transient ;;
      *) fail actions-gateway-activation-failed "Actions Gateway activation failed (rc=$gateway_activation_rc): $(printf '%s' "$gateway_activation_output" | tail -c 300 | tr '\n\"' ' _')" ;;
    esac
    mkdir -p "${gateway_config_state%/*}" \
      || fail actions-gateway-state-failed "cannot create Actions Gateway state directory"
    gateway_config_state_tmp="${gateway_config_state}.$$.tmp"
    printf '%s\n' "$gateway_config_hash" >"$gateway_config_state_tmp" \
      && mv "$gateway_config_state_tmp" "$gateway_config_state" \
      || { rm -f "$gateway_config_state_tmp"; fail actions-gateway-state-failed "cannot publish Actions Gateway config state"; }
  fi
  publish_component_receipt actions-gateway "$deployment_sha" \
    || fail component-receipt-failed "cannot record Actions Gateway convergence"
fi
bash "${DEPLOY}/packaging/install-botmaster-notify.sh" || fail install-botmaster-notify-failed "install-botmaster-notify.sh failed"
bash "${DEPLOY}/packaging/install-web.sh" || fail install-web-failed "install-web.sh failed"
bash "${DEPLOY}/packaging/install-buildbox-parity.sh" >/dev/null \
  || fail install-buildbox-parity-failed "install-buildbox-parity.sh could not arm buildbox-parity.timer"

# Brief canary: prove the incident dispatch-brief pipeline assembles against the assets
# this deploy just made live. The canary skips itself only while the assets directory is
# entirely absent (phased landing); partial or broken assets fail the deploy.
canary_out=$(cd "${DEPLOY}/collector" && OVERDECK_DEPLOY_DIR="$DEPLOY" bun scripts/brief-canary.ts 2>&1) \
  || fail brief-canary-failed "$(printf '%s' "$canary_out" | tr '\n"' ' _' | cut -c1-300)"
printf '%s\n' "$canary_out"

# The sandbox image is built on each box, so a landed Containerfile only reaches the fleet
# when somebody provisions. The tag is the content digest of the image context, so this
# deploy can name the image the landed tree expects. Drift is an installed-fleet tripwire:
# report it as deployed-degraded after local runtime delivery rather than replaying the same
# complete deploy. rc 4 means parity is unknown because a box was unreachable.
sandbox_parity=$(bash "${DEPLOY}/modules/workstation/claude/bin/sandbox-provision" --check --all 2>&1)
sandbox_rc=$?
case "$sandbox_rc" in
  0) ;;
  4)
    printf '%s\n' "$sandbox_parity" >&2
    mark_degraded sandbox-parity-unverified "one or more buildboxes were unreachable during image parity inspection"
    ;;
  *)
    printf '%s\n' "$sandbox_parity" >&2
    mark_degraded sandbox-image-drift "sandbox image parity differs; run ${DEPLOY}/modules/workstation/claude/bin/sandbox-provision --all"
    ;;
esac

# The OOM early-warning feed is produced by root timers whose enablement lives outside the
# repo, in /etc. It was lost once and nothing noticed for weeks, leaving the exhaustion ETA
# frozen through a desktop freeze. Re-assert it on every deploy of a machine already armed.
if [[ -f /etc/systemd/system/node-textfile@.timer ]]; then
  deck-sudo bash "${DEPLOY}/modules/monitor/grafana/install-textfile-timers.sh" >/dev/null ||
    fail textfile-producers-blind "node_exporter textfile producers are not armed and producing — the OOM early-warning ETA is blind"
fi

# The notification gate is a root-installed copy, so a deploy that changes its source
# would otherwise leave the armed emitter stale. Only refreshes a machine already armed.
NOTIF_GATE_SRC="${DEPLOY}/modules/security/notif-gate/notif_gate.py"
NOTIF_GATE_LIVE=/usr/local/lib/notif-gate/notif_gate.py
if [[ -e "$NOTIF_GATE_LIVE" ]] && ! cmp -s "$NOTIF_GATE_SRC" "$NOTIF_GATE_LIVE"; then
  deck-sudo bash "${DEPLOY}/modules/security/notif-gate/install.sh" >/dev/null ||
    fail notif-gate-refresh-failed "installed notification gate is stale and reinstall failed: $NOTIF_GATE_LIVE"
fi

# agent-guard runs from a copy of its source at a stable user path, so a deploy that
# changes its notification emitter would otherwise leave the running guard ungated.
# Only refreshes a machine where the guard is already installed.
AGENT_GUARD_SRC="${DEPLOY}/modules/monitor/agent-guard/src/agent_guard"
AGENT_GUARD_LIVE="$HOME/.local/share/system-monitor/lib/agent_guard"
if [[ -d "$AGENT_GUARD_LIVE" ]] &&
   ! diff -rq --exclude=__pycache__ "$AGENT_GUARD_SRC" "$AGENT_GUARD_LIVE" >/dev/null 2>&1; then
  rm -rf "$AGENT_GUARD_LIVE" && cp -a "$AGENT_GUARD_SRC" "$AGENT_GUARD_LIVE" ||
    fail agent-guard-refresh-failed "cannot refresh the installed guard at $AGENT_GUARD_LIVE"
  systemctl --user restart agent-guard.service ||
    fail agent-guard-restart-failed "agent-guard.service failed to restart after refresh"
fi

# A long-lived reaper-notifier holds the old emitter in memory; only cycle a running one.
# An absent unit means nothing holds a stale emitter, so there is nothing to cycle.
reaper_base=$(component_convergence_base reaper-notifier "${stamp_before:-}" "$deployment_sha" 2>/dev/null || true)
if ! service_unchanged_since_stamp reaper-notifier.service "$reaper_base" "$deployment_sha" "${REAPER_RESTART_PATHS[@]}" &&
   systemctl --user list-unit-files reaper-notifier.service >/dev/null 2>&1 &&
   [[ -n "$(systemctl --user list-unit-files --no-legend reaper-notifier.service 2>/dev/null)" ]]; then
  systemctl --user try-restart reaper-notifier.service ||
    fail reaper-notifier-restart-failed "reaper-notifier.service failed to restart after refresh"
fi
if systemctl --user is-active --quiet reaper-notifier.service; then
  publish_component_receipt reaper-notifier "$deployment_sha" \
    || fail component-receipt-failed "cannot record reaper notifier convergence"
fi

restart_services=()
release_restart_services=()
kept_services=()
if (( collector_work_needed )); then
  restart_services+=(overdeck-collector.service)
else
  kept_services+=(overdeck-collector.service)
fi
if (( controller_work_needed )); then
  restart_services+=(overdeck-controller.service)
else
  kept_services+=(overdeck-controller.service)
fi
if (( botmaster_work_needed )); then
  restart_services+=(botmaster-proxy.service)
else
  kept_services+=(botmaster-proxy.service)
fi
web_base=$(component_convergence_base web "${stamp_before:-}" "$deployment_sha" 2>/dev/null || true)
watchdog_base=$(component_convergence_base web-watchdog "${stamp_before:-}" "$deployment_sha" 2>/dev/null || true)
if [[ "$WEB_BUILD_NEEDED" == 0 && "$web_unit_changed" == 0 ]] && \
   service_unchanged_since_stamp overdeck-web.service "$web_base" "$deployment_sha" \
     packaging/overdeck-web.service packaging/install-web.sh; then
  kept_services+=(overdeck-web.service)
else restart_services+=(overdeck-web.service); release_restart_services+=(overdeck-web.service); fi
if [[ "$watchdog_unit_changed" == 0 ]] && service_unchanged_since_stamp overdeck-web-watchdog.service "$watchdog_base" "$deployment_sha" \
  packaging/web-watchdog.sh packaging/install-web.sh packaging/overdeck-web-watchdog.service; then
  kept_services+=(overdeck-web-watchdog.service)
else restart_services+=(overdeck-web-watchdog.service); release_restart_services+=(overdeck-web-watchdog.service); fi
restart_note="candidate restarted:${restart_services[*]:-none}; candidate kept:${kept_services[*]:-none}"
release_restart_cmd="systemctl --user restart ${release_restart_services[*]}"
((${#release_restart_services[@]})) || release_restart_cmd=true
release_smoke_log="${DEPLOY_STATE_FILE%.json}.smoke.log"
# web-release evaluates this command once per readiness probe. Each pass validates
# collector, controller, botmaster, and every concrete web route by protocol and body
# type; bounded evidence is appended without exposing the shared bearer token.
# The whole command MUST stay a subshell: web-release evals it in its own process, so a
# bare `exit` here kills web-release after one attempt and skips both retries and rollback.
release_verify_cmd="(smoke_out=\$(OVERDECK_DEPLOY_DIR='${DEPLOY}' OVERDECK_READINESS_COLLECTOR_URL='${COLLECTOR_URL}/health' OVERDECK_READINESS_WEB_URL='${WEB_URL}' OVERDECK_READINESS_WEB_PAGES_DIR='${DEPLOY}/apps/web/src/pages' bash '${DEPLOY}/packaging/deploy-readiness.sh' 2>&1); smoke_rc=\$?; { printf '%s %s\\n' \"\$(date +%H:%M:%S)\" \"\$(printf '%s' \"\$smoke_out\" | tail -c 600 | tr '\\n' ' ')\" >> '$release_smoke_log'; tail -n 200 '$release_smoke_log' > '$release_smoke_log.t' && mv '$release_smoke_log.t' '$release_smoke_log'; } 2>/dev/null || true; printf '%s\\n' \"\$smoke_out\"; exit \$smoke_rc)"
: > "$release_smoke_log" 2>/dev/null || true
deploy_state running restarting "$restart_note"
OVERDECK_WEB_RELEASE_VERIFY_TIMEOUT="$SMOKE_READINESS_TIMEOUT" \
  OVERDECK_WEB_RELEASE_VERIFY_DELAY="$SMOKE_READINESS_DELAY" \
  OVERDECK_WEB_RELEASE_RESTART_CMD="$release_restart_cmd" \
  OVERDECK_WEB_RELEASE_VERIFY_CMD="$release_verify_cmd" \
  bash "${DEPLOY}/packaging/web-release.sh" activate "$release" \
  || {
    smoke_tail=$(tail -n 3 "$release_smoke_log" 2>/dev/null | tr '\n"' ' _' | cut -c1-300)
    smoke_detail="candidate restarted:${restart_services[*]:-none}; candidate kept:${kept_services[*]:-none}; restoration restarted:${release_restart_services[*]:-none}; prior web release and grouped service health were restored"
    [[ -z "$smoke_tail" ]] || smoke_detail+="; last smoke: $smoke_tail"
    fail smoke-failed-rolled-back "$smoke_detail"
  }
for component in web web-watchdog; do
  publish_component_receipt "$component" "$deployment_sha" \
    || fail component-receipt-failed "cannot record ${component} convergence"
done
# Cheap proof for every proposed keep: if MainPID moved despite the gate, report reality.
verified_kept=()
for svc in "${kept_services[@]}"; do
  pid_after=$(systemctl --user show -p MainPID --value "$svc" 2>/dev/null || true)
  if [[ -n "${pid_before[$svc]:-}" && "${pid_before[$svc]}" == "$pid_after" ]]; then
    verified_kept+=("$svc")
  else
    restart_services+=("$svc")
  fi
done
kept_services=("${verified_kept[@]}")
case "$kanboard_status" in
  *'kanboard: kept'*) kept_services+=(overdeck-kanboard.service) ;;
  *'kanboard: restarted'*|*'kanboard: fallback-restarted'*) restart_services+=(overdeck-kanboard.service) ;;
esac
if (( gateway_enabled )); then
  if (( gateway_work_needed )); then restart_services+=("$ACTIONS_GATEWAY_SERVICE"); else kept_services+=("$ACTIONS_GATEWAY_SERVICE"); fi
fi
reaper_pid_after=$(systemctl --user show -p MainPID --value reaper-notifier.service 2>/dev/null || true)
if [[ -n "${pid_before[reaper-notifier.service]:-}" ]]; then
  if [[ "${pid_before[reaper-notifier.service]}" == "$reaper_pid_after" ]]; then kept_services+=(reaper-notifier.service); else restart_services+=(reaper-notifier.service); fi
fi
restart_evidence="restarted:${restart_services[*]:-none}; kept:${kept_services[*]:-none}"
bash "${DEPLOY}/packaging/web-release.sh" prune \
  || mark_degraded web-release-prune-failed "retired web releases could not be pruned safely from $RELEASES_DIR"
publish_backend_release_report
collector_health_token=""
if [[ -r "$HOME/.config/overdeck/token" ]]; then
  collector_health_token=$(<"$HOME/.config/overdeck/token")
fi
col=$(curl -sS -o /dev/null -w '%{http_code}' --max-time 5 \
  -H "Authorization: Bearer ${collector_health_token}" "$COLLECTOR_URL/health") || col=000

sha=$(git rev-parse --short HEAD)
# Only requests present when this holder started are satisfied by this deploy.
finalize_deploy_success
terminal_status=deployed
terminal_detail="$restart_evidence; backend_report:$BACKEND_RELEASE_REPORT"
if (( ${#DEGRADED_REASONS[@]} > 0 )); then
  terminal_status=deployed-degraded
fi
emit_deploy_evidence deploy-result --event-key "deploy:${deployment_sha}:${DEPLOY_STARTED_AT}:result" \
  --deployment-id "$DELIVERY_DEPLOYMENT_ID" --release "$deployment_sha" --status "$terminal_status" \
  --readiness passed \
  || mark_degraded delivery-evidence-result-failed "exact request deployment result could not be recorded"
emit_deploy_evidence proof-recorded --event-key "deploy:${deployment_sha}:${DEPLOY_STARTED_AT}:proof" \
  --proof-id "installed:${deployment_sha}:${DEPLOY_STARTED_AT}" --release "$deployment_sha" --status passed \
  --owner-url "/reports#lineage" \
  || mark_degraded delivery-evidence-proof-failed "installed owner proof could not be recorded"
if (( ${#DEGRADED_REASONS[@]} > 0 )); then
  terminal_status=deployed-degraded
  terminal_detail+="; degraded:$(degraded_summary)"
fi
deploy_state finished "$terminal_status" "deployed $sha; $terminal_detail"
landed=$(what_landed "${stamp_before:-}")
rm -f "${DEPLOY_STATE_FILE}.last-notified-failure" 2>/dev/null || true
if [[ "$terminal_status" == deployed-degraded ]]; then
  notify_owner "Deployed $sha to your Overdeck with degraded checks: $(degraded_summary).${landed:+$'\n'$landed}"
else
  notify_owner "Deployed $sha to your Overdeck.${landed:+$'\n'$landed}"
fi
printf '{"stage":"deploy-local","status":"%s","sha":"%s","web":"%s","collector_http":"%s","detail":"%s"}\n' \
  "$terminal_status" "$sha" "$WEB_URL" "$col" "${terminal_detail//\"/\'}"
