#!/usr/bin/env bash

set -euo pipefail
shopt -s inherit_errexit 2>/dev/null || true

SCRIPT_SOURCE="${BASH_SOURCE[0]}"
SCRIPT_DIR="$(cd -- "$(dirname -- "$SCRIPT_SOURCE")" && pwd -P)"
SCRIPT_NAME="$(basename -- "$SCRIPT_SOURCE")"

APP_NAME="agentic"
APP_TITLE="Agentic Installer"
APP_TUI_TITLE="Agentic installer (TUI mode)"
APP_REPO_URL="https://github.com/sawrus/agent-guides.git"
APP_REPO_LINK="https://github.com/sawrus/agent-guides"
PROJECT_MANIFEST_NAME=".agentic.json"

DEFAULT_AGENT_OS="default"
STATIC_AGENT_OS=(opencode codex claude antigravity cursor kilocode)
INSTALL_DIRS=(rules skills workflows prompts)
THEME_CHOICES=(auto dark light)

XDG_CONFIG_HOME="${XDG_CONFIG_HOME:-$HOME/.config}"
XDG_DATA_HOME="${XDG_DATA_HOME:-$HOME/.local/share}"
APP_CONFIG_DIR="$XDG_CONFIG_HOME/$APP_NAME"
APP_CONFIG_FILE="$APP_CONFIG_DIR/config"
OPENCODE_PLUGIN_CONFIG_FILE="$APP_CONFIG_DIR/opencode-plugins.json"
APP_DATA_DIR="$XDG_DATA_HOME/$APP_NAME"
APP_REPO_DIR="$APP_DATA_DIR/repo"

REPO_ROOT=""
AREAS_ROOT=""
EXTENSIONS_ROOT=""
ROOT_AGENTS_FILE=""
THEME_LOADED_FROM_CONFIG=false

DRY_RUN=false
PROJECT_DIR=""
THEME="auto"
THEME_EXPLICIT=false
ACTIVE_THEME="dark"

SELECTED_AGENT_OS=("$DEFAULT_AGENT_OS")
SELECTED_AREAS=()
SELECTED_SPECS=()
INSTALL_SETTINGS_REPLAY=false

SELF_INSTALL_FORCE=false
SELF_INSTALL_BIN_DIR="${HOME}/.local/bin"
SELF_INSTALL_NAME="$APP_NAME"
SELF_INSTALL_WITH_FZF=false

CREATED_PATHS=()
COPIED_PATHS=()
MANAGED_RECORDS=()
SKIPPED_MANAGED_PATHS=()
WARNINGS=()

CONTEXT7_API_KEY="${CONTEXT7_API_KEY:-}"
AGENTIC_ENABLE_CONTEXT7="${AGENTIC_ENABLE_CONTEXT7:-}"
AGENTIC_DOCTOR="${AGENTIC_DOCTOR:-1}"
AGENTIC_DOCTOR_KEEP_TMP="${AGENTIC_DOCTOR_KEEP_TMP:-0}"
AGENTIC_DOCTOR_TIMEOUT_SECONDS="${AGENTIC_DOCTOR_TIMEOUT_SECONDS:-10}"
AGENTIC_MEMPALACE_TIMEOUT_SECONDS="${AGENTIC_MEMPALACE_TIMEOUT_SECONDS:-60}"

OPENCODE_TELEGRAM_ENABLED=""
OPENCODE_TELEGRAM_BOT_TOKEN=""
OPENCODE_TELEGRAM_CHAT_ID=""
OPENCODE_AGENT_MODEL_MAPPER_ENABLED=""
OPENCODE_PLUGINS_CONFIGURED=false

RUN_LOG_ACTIVE=false
RUN_LOG_FILE=""
CHANGED_PATHS_REPORT_FILE=""

COLOR_RESET=""
COLOR_HEADER=""
COLOR_INFO=""
COLOR_WARN=""
COLOR_ERROR=""
COLOR_DIM=""

FZF_COLOR_ARGS=()

usage() {
  cat <<USAGE
$APP_TITLE $(app_version_label)

Usage:
  $SCRIPT_NAME list [agentos|areas|specs --area <name>]
  $SCRIPT_NAME install --project-dir <dir> [--agent-os <comma_list>] --areas <comma_list> --specializations <comma_list> [--theme auto|dark|light]
  $SCRIPT_NAME tui [--theme auto|dark|light]
  $SCRIPT_NAME upgrade
  $SCRIPT_NAME self-install [--bin-dir <dir>] [--force] [--install-fzf] [--dry-run]
  $SCRIPT_NAME --version

Behavior:
  - No arguments in interactive terminal: runs TUI mode
  - No arguments in non-interactive mode: prints usage and exits with code 1
  - Installed mode bootstraps a local knowledge-base checkout on first use

Options:
  --project-dir         Target project directory (created if missing)
  --agent-os            Comma-separated agent OS list (default: ${DEFAULT_AGENT_OS})
  --areas               Comma-separated area list (example: software)
  --specializations     Comma-separated specializations in area.spec format (example: software.backend,software.frontend)
  --theme               Interface theme: auto|dark|light (default: config value or auto)
  --no-doctor           Skip real agent smoke checks after install
  --bin-dir             Installation directory for self-install (default: ~/.local/bin)
  --force               Overwrite existing binary for self-install
  --install-fzf         During self-install, try to auto-install fzf (optional)
  --dry-run             Show actions without writing files
  -h, --help            Show this help
  -V, --version         Show agentic version

Examples:
  $SCRIPT_NAME list agentos
  $SCRIPT_NAME install --project-dir /tmp/demo --agent-os opencode,codex --areas software --specializations software.backend,software.frontend
  $SCRIPT_NAME tui --theme dark
  $SCRIPT_NAME upgrade
  $SCRIPT_NAME self-install --force
USAGE
}

read_package_version() {
  local package_file="$1"
  [[ -f "$package_file" ]] || return 1

  sed -n 's/^[[:space:]]*"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "$package_file" | head -n 1
}

app_version() {
  local version=""
  local candidate
  for candidate in "$SCRIPT_DIR/package.json" "$REPO_ROOT/package.json" "$APP_REPO_DIR/package.json"; do
    [[ -n "$candidate" ]] || continue
    version="$(read_package_version "$candidate" || true)"
    if [[ -n "$version" ]]; then
      printf '%s\n' "$version"
      return
    fi
  done
  printf 'unknown\n'
}

app_version_label() {
  printf 'v%s\n' "$(app_version)"
}

is_interactive_terminal() {
  if [[ "${AGENTIC_FORCE_INTERACTIVE:-${AGENTOS_FORCE_INTERACTIVE:-}}" == "1" ]]; then
    return 0
  fi
  [[ -t 0 && -t 1 ]]
}

supports_color() {
  [[ -t 1 ]] || return 1
  [[ -n "${NO_COLOR:-}" ]] && return 1
  command -v tput >/dev/null 2>&1 || return 1
  local colors
  colors="$(tput colors 2>/dev/null || echo 0)"
  [[ "$colors" =~ ^[0-9]+$ ]] || return 1
  (( colors >= 8 ))
}

detect_platform() {
  if [[ -n "${AGENTIC_PLATFORM_OVERRIDE:-${AGENTOS_PLATFORM_OVERRIDE:-}}" ]]; then
    echo "${AGENTIC_PLATFORM_OVERRIDE:-${AGENTOS_PLATFORM_OVERRIDE:-}}"
    return
  fi

  local uname_s
  uname_s="$(uname -s 2>/dev/null || echo unknown)"
  case "$uname_s" in
    Linux)
      echo "linux"
      ;;
    Darwin)
      echo "macos"
      ;;
    CYGWIN*|MINGW*|MSYS*)
      echo "windows"
      ;;
    *)
      echo "unknown"
      ;;
  esac
}

detect_auto_theme() {
  local bg
  bg="${COLORFGBG:-}"
  if [[ "$bg" =~ \;([0-9]{1,2})$ ]]; then
    local code="${BASH_REMATCH[1]}"
    case "$code" in
      0|1|2|3|4|5|6|8|15)
        echo "light"
        return
        ;;
      *)
        echo "dark"
        return
        ;;
    esac
  fi
  echo "dark"
}

set_theme_colors() {
  COLOR_RESET=""
  COLOR_HEADER=""
  COLOR_INFO=""
  COLOR_WARN=""
  COLOR_ERROR=""
  COLOR_DIM=""
  FZF_COLOR_ARGS=()
  local use_ansi=false
  if supports_color; then
    use_ansi=true
  fi

  local resolved="$THEME"
  if [[ "$resolved" == "auto" ]]; then
    resolved="$(detect_auto_theme)"
  fi
  ACTIVE_THEME="$resolved"

  case "$ACTIVE_THEME" in
    light)
      if [[ "$use_ansi" == true ]]; then
        COLOR_RESET=$'\033[0m'
        COLOR_HEADER=$'\033[1;34m'
        COLOR_INFO=$'\033[1;36m'
        COLOR_WARN=$'\033[1;33m'
        COLOR_ERROR=$'\033[1;31m'
        COLOR_DIM=$'\033[2;30m'
      fi
      FZF_COLOR_ARGS=(
        "--color=fg:#1f2937,bg:#f8fafc,hl:#2563eb"
        "--color=fg+:#111827,bg+:#dbeafe,hl+:#1d4ed8"
        "--color=prompt:#0f766e,pointer:#dc2626,marker:#16a34a,spinner:#2563eb,header:#334155"
      )
      ;;
    dark|*)
      ACTIVE_THEME="dark"
      if [[ "$use_ansi" == true ]]; then
        COLOR_RESET=$'\033[0m'
        COLOR_HEADER=$'\033[1;36m'
        COLOR_INFO=$'\033[1;32m'
        COLOR_WARN=$'\033[1;33m'
        COLOR_ERROR=$'\033[1;31m'
        COLOR_DIM=$'\033[2;37m'
      fi
      FZF_COLOR_ARGS=(
        "--color=fg:#e5e7eb,bg:#111827,hl:#60a5fa"
        "--color=fg+:#ffffff,bg+:#1f2937,hl+:#93c5fd"
        "--color=query:#e5e7eb,prompt:#22c55e,pointer:#f97316,marker:#a3e635,spinner:#06b6d4,header:#d1d5db"
      )
      ;;
  esac
}

log() {
  emit_log_line stdout "[agentic]" "$1" "$COLOR_INFO"
}

warn() {
  emit_log_line stdout "[agentic][warn]" "$1" "$COLOR_WARN"
  WARNINGS+=("$1")
}

error() {
  emit_log_line stderr "[agentic][error]" "$1" "$COLOR_ERROR"
}

timestamp_now() {
  date '+%Y-%m-%d %H:%M:%S'
}

init_run_logging() {
  if [[ "$RUN_LOG_ACTIVE" == true ]]; then
    return
  fi

  local base_dir="${TMPDIR:-/tmp}"
  local stamp
  stamp="$(date '+%Y%m%d-%H%M%S')"
  RUN_LOG_FILE="$(mktemp "$base_dir/agentic-$stamp.XXXXXX")"
  CHANGED_PATHS_REPORT_FILE="$RUN_LOG_FILE.changes"
  RUN_LOG_ACTIVE=true
  log "Run log initialized: $RUN_LOG_FILE"
}

write_run_log_line() {
  local line="$1"
  if [[ "$RUN_LOG_ACTIVE" == true && -n "$RUN_LOG_FILE" ]]; then
    printf '%s\n' "$line" >> "$RUN_LOG_FILE"
  fi
}

emit_log_line() {
  local stream="$1"
  local tag="$2"
  local message="$3"
  local color="${4:-}"

  local line plain_line ts
  if [[ "$RUN_LOG_ACTIVE" == true ]]; then
    ts="$(timestamp_now)"
    plain_line="$ts $tag $message"
    if [[ -n "$color" ]]; then
      line="$ts ${color}${tag}${COLOR_RESET} $message"
    else
      line="$plain_line"
    fi
  else
    plain_line="$tag $message"
    if [[ -n "$color" ]]; then
      line="${color}${tag}${COLOR_RESET} $message"
    else
      line="$plain_line"
    fi
  fi

  if [[ "$stream" == "stderr" ]]; then
    printf '%s\n' "$line" >&2
  else
    printf '%s\n' "$line"
  fi
  write_run_log_line "$plain_line"
}

out() {
  local message="${1:-}"
  local color="${2:-}"
  local line plain_line ts

  if [[ -z "$message" ]]; then
    printf '\n'
    write_run_log_line ""
    return
  fi

  if [[ "$RUN_LOG_ACTIVE" == true ]]; then
    ts="$(timestamp_now)"
    plain_line="$ts $message"
    if [[ -n "$color" ]]; then
      line="$ts ${color}${message}${COLOR_RESET}"
    else
      line="$plain_line"
    fi
  else
    plain_line="$message"
    if [[ -n "$color" ]]; then
      line="${color}${message}${COLOR_RESET}"
    else
      line="$plain_line"
    fi
  fi

  printf '%s\n' "$line"
  write_run_log_line "$plain_line"
}

log_file_block() {
  local label="$1"
  local path="$2"
  [[ "$RUN_LOG_ACTIVE" == true && -n "$RUN_LOG_FILE" && -f "$path" ]] || return 0

  write_run_log_line "$(timestamp_now) --- $label output begin ---"
  while IFS= read -r line || [[ -n "$line" ]]; do
    write_run_log_line "$(timestamp_now) $line"
  done < "$path"
  write_run_log_line "$(timestamp_now) --- $label output end ---"
}

write_changed_paths_report() {
  if [[ -z "$CHANGED_PATHS_REPORT_FILE" ]]; then
    local base_dir="${TMPDIR:-/tmp}"
    local stamp
    stamp="$(date '+%Y%m%d-%H%M%S')"
    CHANGED_PATHS_REPORT_FILE="$(mktemp "$base_dir/agentic-changed-paths-$stamp.XXXXXX")"
  fi

  {
    printf 'Agentic changed paths report\n'
    printf 'Generated at: %s\n' "$(timestamp_now)"
    printf 'Project dir: %s\n' "$PROJECT_DIR"
    printf 'Knowledge base repo: %s\n' "$REPO_ROOT"
    printf '\n'

    printf 'Created directories (%s)\n' "${#CREATED_PATHS[@]}"
    if [[ "${#CREATED_PATHS[@]}" -eq 0 ]]; then
      printf -- '- (none)\n'
    else
      local created_path
      for created_path in "${CREATED_PATHS[@]}"; do
        printf -- '- %s\n' "$created_path"
      done
    fi
    printf '\n'

    printf 'Copied/generated paths (%s)\n' "${#COPIED_PATHS[@]}"
    if [[ "${#COPIED_PATHS[@]}" -eq 0 ]]; then
      printf -- '- (none)\n'
    else
      local copied_path
      for copied_path in "${COPIED_PATHS[@]}"; do
        printf -- '- %s\n' "$copied_path"
      done
    fi
    printf '\n'

    printf 'Warnings (%s)\n' "${#WARNINGS[@]}"
    if [[ "${#WARNINGS[@]}" -eq 0 ]]; then
      printf -- '- (none)\n'
    else
      local warning
      for warning in "${WARNINGS[@]}"; do
        printf -- '- %s\n' "$warning"
      done
    fi
  } > "$CHANGED_PATHS_REPORT_FILE"
}

unique_append() {
  local value="$1"
  local arr_name="$2"
  local item
  eval "for item in \"\${${arr_name}[@]:-}\"; do
    if [[ \"\$item\" == \"\$value\" ]]; then
      return
    fi
  done"
  eval "${arr_name}+=(\"\$value\")"
}

trim() {
  local s="$1"
  s="${s#${s%%[![:space:]]*}}"
  s="${s%${s##*[![:space:]]}}"
  printf '%s\n' "$s"
}

readlines() {
  local arr_name="$1"
  local line
  while IFS= read -r line || [[ -n "$line" ]]; do
    eval "${arr_name}+=(\"\$line\")"
  done
}

split_csv() {
  local raw="$1"
  local arr_name="$2"
  local part
  local parts=()
  IFS=',' read -r -a parts <<< "$raw"
  for part in "${parts[@]}"; do
    part="$(trim "$part")"
    [[ -n "$part" ]] && eval "${arr_name}+=(\"\$part\")"
  done
}

validate_theme() {
  local theme="$1"
  local item
  for item in "${THEME_CHOICES[@]}"; do
    if [[ "$item" == "$theme" ]]; then
      return 0
    fi
  done
  return 1
}

is_repo_root() {
  local dir="$1"
  [[ -n "$dir" ]] || return 1
  [[ -d "$dir/areas" ]] && [[ -d "$dir/extensions" ]] && [[ -f "$dir/AGENTS.md" ]]
}

resolve_dev_repo_root() {
  local candidates=(
    "$SCRIPT_DIR"
    "$(cd -- "$SCRIPT_DIR/.." 2>/dev/null && pwd -P || true)"
  )
  local candidate
  for candidate in "${candidates[@]}"; do
    [[ -n "$candidate" ]] || continue
    if is_repo_root "$candidate"; then
      printf '%s\n' "$candidate"
      return 0
    fi
  done
  return 1
}

resolve_data_repo_root() {
  if is_repo_root "$APP_REPO_DIR"; then
    printf '%s\n' "$APP_REPO_DIR"
    return 0
  fi
  return 1
}

refresh_repo_paths() {
  local resolved_root=""
  if resolved_root="$(resolve_dev_repo_root 2>/dev/null)"; then
    :
  elif resolved_root="$(resolve_data_repo_root 2>/dev/null)"; then
    :
  else
    resolved_root="$APP_REPO_DIR"
  fi

  REPO_ROOT="$resolved_root"
  AREAS_ROOT="$REPO_ROOT/areas"
  EXTENSIONS_ROOT="$REPO_ROOT/extensions"
  ROOT_AGENTS_FILE="$REPO_ROOT/AGENTS.md"
}

ensure_repo_layout() {
  refresh_repo_paths
  if ! is_repo_root "$REPO_ROOT"; then
    error "knowledge base checkout is not available at '$REPO_ROOT'"
    error "Run '$APP_NAME upgrade' or any supported command again after bootstrap succeeds."
    exit 1
  fi
}

ensure_git_available() {
  if ! command -v git >/dev/null 2>&1; then
    error "git is required to bootstrap or upgrade the local knowledge base checkout"
    exit 1
  fi
}

clone_repo_checkout() {
  ensure_git_available
  ensure_dir "$APP_DATA_DIR"

  if [[ -e "$APP_REPO_DIR" ]]; then
    warn "Replacing invalid knowledge base checkout at $APP_REPO_DIR"
    if [[ "$DRY_RUN" == true ]]; then
      log "DRY-RUN rm -rf $APP_REPO_DIR"
    else
      rm -rf -- "$APP_REPO_DIR"
    fi
  fi

  if [[ "$DRY_RUN" == true ]]; then
    log "DRY-RUN git clone $APP_REPO_URL $APP_REPO_DIR"
    return
  fi

  log "Bootstrapping knowledge base checkout into $APP_REPO_DIR"
  git clone "$APP_REPO_URL" "$APP_REPO_DIR"
  refresh_repo_paths
  if ! is_repo_root "$APP_REPO_DIR"; then
    error "Bootstrapped checkout is invalid: expected areas/, extensions/, and AGENTS.md in $APP_REPO_DIR"
    exit 1
  fi
}

ensure_repo_checkout() {
  refresh_repo_paths
  if is_repo_root "$REPO_ROOT"; then
    return 0
  fi
  clone_repo_checkout
  refresh_repo_paths
  ensure_repo_layout
}

ensure_repo_available_for_command() {
  if resolve_dev_repo_root >/dev/null 2>&1; then
    refresh_repo_paths
    return
  fi
  ensure_repo_checkout
}

load_user_config() {
  local line key value
  THEME_LOADED_FROM_CONFIG=false

  if [[ ! -f "$APP_CONFIG_FILE" ]]; then
    return
  fi

  while IFS= read -r line || [[ -n "$line" ]]; do
    line="$(trim "$line")"
    [[ -z "$line" ]] && continue
    [[ "$line" == \#* ]] && continue
    if [[ "$line" != *=* ]]; then
      warn "Ignoring malformed config line in $APP_CONFIG_FILE: $line"
      continue
    fi

    key="${line%%=*}"
    value="${line#*=}"
    key="$(trim "$key")"
    value="$(trim "$value")"

    case "$key" in
      theme)
        if validate_theme "$value"; then
          THEME="$value"
          THEME_LOADED_FROM_CONFIG=true
        else
          warn "Ignoring invalid theme '$value' in $APP_CONFIG_FILE; falling back to auto"
          THEME="auto"
        fi
        ;;
      *)
        ;;
    esac
  done < "$APP_CONFIG_FILE"
}

save_user_config() {
  ensure_dir "$APP_CONFIG_DIR"

  if [[ "$DRY_RUN" == true ]]; then
    log "DRY-RUN write config to $APP_CONFIG_FILE"
    return
  fi

  cat > "$APP_CONFIG_FILE" <<CONFIG
# $APP_NAME user configuration
theme=$THEME
CONFIG
  unique_append "$APP_CONFIG_FILE" COPIED_PATHS
}

# Agent-specific directory mappings using case statements for Bash 3.2 compatibility
get_agent_dir_mapping() {
  local agent_os="$1"
  case "$agent_os" in
    opencode)     echo ".opencode/rules .opencode/skills .opencode/commands -" ;;
    cursor)       echo ".cursor/rules .cursor/skills - -" ;;
    kilocode)     echo ".kilocode/rules .kilocode/skills .kilocode/workflows -" ;;
    antigravity)  echo ".kilocode/rules .kilocode/skills .kilocode/workflows -" ;;
    *)            echo "" ;;
  esac
}

get_dest_dir() {
  local agent_os="$1"
  local bucket="$2"

  local mapping
  mapping="$(get_agent_dir_mapping "$agent_os")"
  if [[ -n "$mapping" ]]; then
    local parts=()
    read -r -a parts <<< "$mapping"
    local dir
    case "$bucket" in
      rules)     dir="${parts[0]}" ;;
      skills)    dir="${parts[1]}" ;;
      workflows) dir="${parts[2]}" ;;
      prompts)   dir="${parts[3]:-}" ;;
      *)         dir=".agent/$bucket" ;;
    esac
    printf '%s\n' "$dir"
  else
    printf '%s\n' ".agent/$bucket"
  fi
}

get_dynamic_agentos() {
  if [[ -d "$EXTENSIONS_ROOT" ]]; then
    find "$EXTENSIONS_ROOT" -mindepth 1 -maxdepth 1 -type d -exec basename {} \; | sort
  fi
}

get_agentos_choices() {
  local seen=()
  local name
  for name in "${STATIC_AGENT_OS[@]}"; do
    unique_append "$name" seen
  done
  while IFS= read -r name; do
    [[ -z "$name" ]] && continue
    unique_append "$name" seen
  done < <(get_dynamic_agentos)
  printf '%s\n' "${seen[@]}"
}

list_areas() {
  find "$AREAS_ROOT" -mindepth 1 -maxdepth 1 -type d -exec basename {} \; | sort | grep -v '^template$'
}

list_specs() {
  local area="$1"
  find "$AREAS_ROOT/$area" -mindepth 1 -maxdepth 1 -type d -exec basename {} \; | sort
}

ensure_dir() {
  local path="$1"
  if [[ "$DRY_RUN" == true ]]; then
    log "DRY-RUN mkdir -p $path"
  else
    mkdir -p "$path"
  fi
  unique_append "$path" CREATED_PATHS
}

ensure_python_available() {
  if ! command -v python3 >/dev/null 2>&1; then
    error "python3 is required for managed install metadata and generated markers"
    exit 1
  fi
}

pip_command() {
  if command -v pip >/dev/null 2>&1; then
    printf '%s\n' "pip"
    return 0
  fi
  if command -v pip3 >/dev/null 2>&1; then
    printf '%s\n' "pip3"
    return 0
  fi
  if command -v python3 >/dev/null 2>&1 && python3 -m pip --version >/dev/null 2>&1; then
    printf '%s\n' "python3 -m pip"
    return 0
  fi
  return 1
}

ensure_pip_available() {
  if ! pip_command >/dev/null; then
    error "pip is required to run agentic install/tui. Install pip for Python 3 and make 'pip3', 'pip', or 'python3 -m pip' available."
    exit 1
  fi
}

ensure_hash_available() {
  if ! command -v shasum >/dev/null 2>&1 && ! command -v sha256sum >/dev/null 2>&1; then
    error "shasum or sha256sum is required to track managed files"
    exit 1
  fi
}

ensure_agentic_runtime_requirements() {
  ensure_python_available
  ensure_pip_available
  ensure_hash_available
}

selected_agent_os_contains() {
  local expected="$1"
  local agent
  for agent in "${SELECTED_AGENT_OS[@]}"; do
    if [[ "$agent" == "$expected" ]]; then
      return 0
    fi
  done
  return 1
}

project_manifest_path() {
  printf '%s\n' "$PROJECT_DIR/$PROJECT_MANIFEST_NAME"
}

normalize_project_dir_path() {
  local raw="$1"
  local parent base parent_real

  if [[ -z "$raw" ]]; then
    printf '%s\n' "$raw"
    return
  fi

  if [[ -d "$raw" ]]; then
    (cd -- "$raw" && pwd -P)
    return
  fi

  parent="$(dirname -- "$raw")"
  base="$(basename -- "$raw")"
  if [[ -d "$parent" ]]; then
    parent_real="$(cd -- "$parent" && pwd -P)" || {
      printf '%s\n' "$raw"
      return
    }
    printf '%s/%s\n' "$parent_real" "$base"
    return
  fi

  printf '%s\n' "$raw"
}

project_rel_path() {
  local path="$1"
  local rel="${path#"$PROJECT_DIR"/}"
  printf '%s\n' "$rel"
}

hash_file() {
  local path="$1"
  if command -v shasum >/dev/null 2>&1; then
    shasum -a 256 "$path" | awk '{print $1}'
  elif command -v sha256sum >/dev/null 2>&1; then
    sha256sum "$path" | awk '{print $1}'
  else
    ensure_hash_available
  fi
}

manifest_has_path() {
  local rel="$1"
  local manifest
  manifest="$(project_manifest_path)"
  [[ -f "$manifest" ]] || return 1
  python3 - "$manifest" "$rel" <<'PY'
import json
import sys
from pathlib import Path

manifest = Path(sys.argv[1])
rel = sys.argv[2]
try:
    data = json.loads(manifest.read_text(encoding="utf-8"))
except Exception:
    sys.exit(1)
for item in data.get("managed_files", []):
    if item.get("path") == rel:
        sys.exit(0)
sys.exit(1)
PY
}

manifest_hash_for_path() {
  local rel="$1"
  local manifest
  manifest="$(project_manifest_path)"
  [[ -f "$manifest" ]] || return 0
  python3 - "$manifest" "$rel" <<'PY'
import json
import sys
from pathlib import Path

manifest = Path(sys.argv[1])
rel = sys.argv[2]
try:
    data = json.loads(manifest.read_text(encoding="utf-8"))
except Exception:
    sys.exit(0)
for item in data.get("managed_files", []):
    if item.get("path") == rel:
        print(item.get("content_hash", ""))
        break
PY
}

can_write_managed_file() {
  local dest="$1"
  local rel
  rel="$(project_rel_path "$dest")"

  if [[ -f "$(project_manifest_path)" ]]; then
    if ! manifest_has_path "$rel"; then
      if [[ -e "$dest" ]]; then
        warn "Skipping unmanaged target on rerun: $rel"
        unique_append "$rel" SKIPPED_MANAGED_PATHS
        return 1
      fi
      return 0
    fi

    if [[ -f "$dest" ]]; then
      local expected_hash current_hash
      expected_hash="$(manifest_hash_for_path "$rel")"
      if [[ -n "$expected_hash" ]]; then
        current_hash="$(hash_file "$dest")"
        if [[ "$current_hash" != "$expected_hash" ]]; then
          warn "Skipping user-modified managed file: $rel"
          unique_append "$rel" SKIPPED_MANAGED_PATHS
          return 1
        fi
      fi
    fi
  fi

  return 0
}

register_managed_file() {
  local dest="$1"
  local source_ref="$2"
  local marker="$3"
  local copied="${4:-true}"
  local rel
  rel="$(project_rel_path "$dest")"
  local digest
  digest="$(hash_file "$dest")"
  MANAGED_RECORDS+=("$rel|$source_ref|$digest|$marker")
  if [[ "$copied" == true ]]; then
    unique_append "$dest" COPIED_PATHS
  fi
}

record_agentic_event() {
  local kind="$1"
  local value="${2:-}"

  case "$kind" in
    DIR)
      unique_append "$value" CREATED_PATHS
      ;;
    COPIED)
      unique_append "$value" COPIED_PATHS
      ;;
    RECORD)
      MANAGED_RECORDS+=("$value")
      ;;
    SKIP)
      unique_append "$value" SKIPPED_MANAGED_PATHS
      ;;
    WARN)
      warn "$value"
      ;;
  esac
}

write_file_with_agentic_marker() {
  local src="$1"
  local dest="$2"
  local source_ref="$3"

  if [[ "$DRY_RUN" == true ]]; then
    log "DRY-RUN write managed file $dest"
    unique_append "$dest" COPIED_PATHS
    return
  fi

  can_write_managed_file "$dest" || return 0

  ensure_dir "$(dirname -- "$dest")"
  local write_status
  write_status="$(python3 - "$src" "$dest" "$source_ref" "$APP_REPO_LINK" "$(app_version_label)" <<'PY'
import json
import re
import sys
from pathlib import Path

src = Path(sys.argv[1])
dest = Path(sys.argv[2])
source_ref = sys.argv[3]
repo = sys.argv[4]
version = sys.argv[5]
text = src.read_text(encoding="utf-8")
suffix = dest.suffix.lower()
marker = f"Generated by agentic; source: {source_ref}; repository: {repo}"


def yaml_quote(value: str) -> str:
    return json.dumps(value, ensure_ascii=False)


def existing_created_by() -> str:
    if not dest.exists():
        return version
    try:
        old = dest.read_text(encoding="utf-8")
    except Exception:
        return version
    match = re.search(r"(?m)^  created_by:\s*(.+?)\s*$", old)
    if not match:
        return version
    return match.group(1).strip().strip('"')


def markdown_with_marker(body: str) -> str:
    created_by = existing_created_by()
    block = (
        "agentic:\n"
        "  generated_by: agentic\n"
        f"  source: {yaml_quote(source_ref)}\n"
        f"  repository: {yaml_quote(repo)}\n"
        f"  created_by: {yaml_quote(created_by)}\n"
        f"  updated_by: {yaml_quote(version)}\n"
    )
    if body.startswith("---\n"):
        end = body.find("\n---", 4)
        if end != -1:
            return body[: end + 1] + block + body[end + 1 :]
    return (
        "---\n"
        + block
        + "---\n"
        + body
    )


def commented(body: str, prefix: str) -> str:
    line = f"{prefix} {marker}\n"
    if body.startswith("#!"):
        first, sep, rest = body.partition("\n")
        if sep:
            return first + sep + line + rest
    return line + body


if suffix == ".md":
    output = markdown_with_marker(text)
elif suffix == ".json":
    data = json.loads(text)
    if not isinstance(data, dict):
        raise SystemExit(f"Cannot add agentic metadata to non-object JSON: {dest}")
    output = json.dumps(data, indent=2, ensure_ascii=False) + "\n"
elif suffix in {".ts", ".tsx", ".js", ".jsx", ".css"}:
    output = commented(text, "//")
elif suffix in {".sh", ".toml", ".py", ".yml", ".yaml"}:
    output = commented(text, "#")
else:
    output = commented(text, "#")

if dest.exists():
    try:
        if dest.read_text(encoding="utf-8") == output:
            print("unchanged")
            raise SystemExit(0)
    except UnicodeDecodeError:
        pass

dest.write_text(output, encoding="utf-8")
print("written")
PY
)"
  if [[ "$write_status" == "unchanged" ]]; then
    register_managed_file "$dest" "$source_ref" "internal" false
  else
    register_managed_file "$dest" "$source_ref" "internal"
  fi
}

write_agentic_manifest() {
  local project_dir="$1"
  local manifest="$project_dir/$PROJECT_MANIFEST_NAME"

  if [[ "$DRY_RUN" == true ]]; then
    log "DRY-RUN write $manifest"
    return
  fi

  local records_file skipped_file
  records_file="$(mktemp "${TMPDIR:-/tmp}/agentic-records.XXXXXX")"
  skipped_file="$(mktemp "${TMPDIR:-/tmp}/agentic-skipped.XXXXXX")"
  if [[ "${#MANAGED_RECORDS[@]}" -gt 0 ]]; then
    printf '%s\n' "${MANAGED_RECORDS[@]}" > "$records_file"
  else
    : > "$records_file"
  fi
  if [[ "${#SKIPPED_MANAGED_PATHS[@]}" -gt 0 ]]; then
    printf '%s\n' "${SKIPPED_MANAGED_PATHS[@]}" > "$skipped_file"
  else
    : > "$skipped_file"
  fi

  local agent_os_csv areas_csv specs_csv mcp_integrations_csv
  local old_ifs="$IFS"
  IFS=,
  agent_os_csv="${SELECTED_AGENT_OS[*]}"
  areas_csv="${SELECTED_AREAS[*]}"
  specs_csv="${SELECTED_SPECS[*]}"
  IFS="$old_ifs"

  # Build mcp_integrations list from current env selections
  local mcp_integrations=()
  if [[ "${AGENTIC_ENABLE_CONTEXT7:-}" =~ ^[Yy](es)?$ ]]; then
    mcp_integrations+=("context7")
  fi
  if [[ "${AGENTIC_ENABLE_MEMPALACE:-}" =~ ^[Yy](es)?$ ]]; then
    mcp_integrations+=("mempalace")
  fi
  old_ifs="$IFS"
  IFS=,
  mcp_integrations_csv="${mcp_integrations[*]:-}"
  IFS="$old_ifs"

  local manifest_status
  manifest_status="$(python3 - "$manifest" "$records_file" "$skipped_file" "$APP_REPO_LINK" "$REPO_ROOT" "$agent_os_csv" "$areas_csv" "$specs_csv" "$(app_version_label)" "$mcp_integrations_csv" "$OPENCODE_TELEGRAM_ENABLED" "$OPENCODE_TELEGRAM_BOT_TOKEN" "$OPENCODE_TELEGRAM_CHAT_ID" "$OPENCODE_AGENT_MODEL_MAPPER_ENABLED" <<'PY'
import json
import sys
from datetime import datetime, timezone
from pathlib import Path

manifest = Path(sys.argv[1])
records_file = Path(sys.argv[2])
skipped_file = Path(sys.argv[3])
repo_link = sys.argv[4]
repo_root = sys.argv[5]
agent_os = [x for x in sys.argv[6].split(",") if x]
areas = [x for x in sys.argv[7].split(",") if x]
specs = [x for x in sys.argv[8].split(",") if x]
app_version = sys.argv[9]
mcp_integrations = [x for x in sys.argv[10].split(",") if x] if len(sys.argv) > 10 else []
telegram_enabled = sys.argv[11].lower() == "true" if len(sys.argv) > 11 and sys.argv[11] else None
telegram_bot_token = sys.argv[12] if len(sys.argv) > 12 else ""
telegram_chat_id = sys.argv[13] if len(sys.argv) > 13 else ""
mapper_enabled = sys.argv[14].lower() == "true" if len(sys.argv) > 14 and sys.argv[14] else None
now = datetime.now(timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z")

existing = {}
created_at = now
old_data = None
if manifest.exists():
    try:
        old = json.loads(manifest.read_text(encoding="utf-8"))
        old_data = old
        created_at = old.get("created_at", created_at)
        for item in old.get("managed_files", []):
            if item.get("path"):
                existing[item["path"]] = item
    except Exception:
        existing = {}
original_existing = json.loads(json.dumps(existing))

for line in records_file.read_text(encoding="utf-8").splitlines():
    if not line:
        continue
    path, source, digest, marker = (line.split("|", 3) + ["", "", "", ""])[:4]
    old_item = original_existing.get(path, {})
    old_updated_at = old_item.get("updated_at", now)
    if (
        old_item.get("source") == source
        and old_item.get("content_hash") == digest
        and old_item.get("marker") == marker
    ):
        item_updated_at = old_updated_at
    else:
        item_updated_at = now
    existing[path] = {
        "path": path,
        "source": source,
        "content_hash": digest,
        "marker": marker,
        "updated_at": item_updated_at,
    }

skipped = [x for x in skipped_file.read_text(encoding="utf-8").splitlines() if x]
old_agentic = old_data.get("_agentic", {}) if isinstance(old_data, dict) else {}
old_settings = old_data.get("settings", {}) if isinstance(old_data, dict) else {}
created_by = old_agentic.get("created_by", app_version)
opencode_plugins = old_settings.get("opencode_plugins", {}) if isinstance(old_settings, dict) else {}
if not isinstance(opencode_plugins, dict):
    opencode_plugins = {}
telegram = opencode_plugins.get("telegram", {}) if isinstance(opencode_plugins.get("telegram"), dict) else {}
agent_model_mapper = opencode_plugins.get("agentModelMapper", {}) if isinstance(opencode_plugins.get("agentModelMapper"), dict) else {}
if telegram_enabled is not None:
    telegram = {
        "enabled": telegram_enabled,
        "botToken": telegram_bot_token,
        "chatId": telegram_chat_id,
    }
if mapper_enabled is not None:
    agent_model_mapper = {"enabled": mapper_enabled}
if telegram or agent_model_mapper:
    opencode_plugins = {
        "telegram": telegram or {"enabled": False},
        "agentModelMapper": agent_model_mapper or {"enabled": False},
    }
data = {
    "_agentic": {
        "generated_by": "agentic",
        "repository": repo_link,
        "created_by": created_by,
        "updated_by": app_version,
    },
    "version": 1,
    "created_at": created_at,
    "updated_at": now,
    "settings": {
        "agent_os": agent_os,
        "areas": areas,
        "specializations": specs,
        "mcp_integrations": mcp_integrations,
        "opencode_plugins": opencode_plugins,
        "source_repo": repo_link,
        "source_checkout": repo_root,
    },
    "managed_files": sorted(existing.values(), key=lambda x: x["path"]),
    "skipped_files": skipped,
}

if old_data is not None:
    old_compare = json.loads(json.dumps(old_data))
    new_compare = json.loads(json.dumps(data))
    for payload in (old_compare, new_compare):
        payload.pop("updated_at", None)
        if isinstance(payload.get("_agentic"), dict):
            payload["_agentic"].pop("updated_by", None)
    if old_compare == new_compare:
        print("unchanged")
        raise SystemExit(0)

manifest.write_text(json.dumps(data, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
print("written")
PY
)"
  rm -f "$records_file" "$skipped_file"
  if [[ "$manifest_status" == "unchanged" ]]; then
    return
  fi
  unique_append "$manifest" COPIED_PATHS
}

load_install_settings_from_manifest() {
  local manifest="$1"
  [[ -f "$manifest" ]] || return 1
  ensure_python_available
  INSTALL_SETTINGS_REPLAY=true

  local values=()
  readlines values < <(python3 - "$manifest" <<'PY'
import json
import sys
from pathlib import Path

data = json.loads(Path(sys.argv[1]).read_text(encoding="utf-8"))
settings = data.get("settings", {})
for key in ("agent_os", "areas", "specializations", "mcp_integrations"):
    print("::" + key)
    for value in settings.get(key, []):
        print(value)
plugins = settings.get("opencode_plugins", {})
if isinstance(plugins, dict):
    telegram = plugins.get("telegram", {})
    if isinstance(telegram, dict):
        print("::opencode_telegram_enabled")
        if telegram.get("enabled") is not None:
            print("true" if telegram.get("enabled") is True else "false")
        print("::opencode_telegram_bot_token")
        if telegram.get("botToken"):
            print(telegram.get("botToken"))
        print("::opencode_telegram_chat_id")
        if telegram.get("chatId"):
            print(telegram.get("chatId"))
    mapper = plugins.get("agentModelMapper", {})
    if isinstance(mapper, dict):
        print("::opencode_agent_model_mapper_enabled")
        if mapper.get("enabled") is not None:
            print("true" if mapper.get("enabled") is True else "false")
PY
)

  local section=""
  local loaded_agent_os=()
  local loaded_areas=()
  local loaded_specs=()
  local loaded_mcp_integrations=()
  local loaded_telegram_enabled=""
  local loaded_telegram_bot_token=""
  local loaded_telegram_chat_id=""
  local loaded_mapper_enabled=""
  local value
  for value in "${values[@]}"; do
    case "$value" in
      "::agent_os") section="agent_os" ;;
      "::areas") section="areas" ;;
      "::specializations") section="specializations" ;;
      "::mcp_integrations") section="mcp_integrations" ;;
      *)
        case "$section" in
          agent_os) loaded_agent_os+=("$value") ;;
          areas) loaded_areas+=("$value") ;;
          specializations) loaded_specs+=("$value") ;;
          mcp_integrations) loaded_mcp_integrations+=("$value") ;;
          opencode_telegram_enabled) loaded_telegram_enabled="$value" ;;
          opencode_telegram_bot_token) loaded_telegram_bot_token="$value" ;;
          opencode_telegram_chat_id) loaded_telegram_chat_id="$value" ;;
          opencode_agent_model_mapper_enabled) loaded_mapper_enabled="$value" ;;
        esac
        ;;
    esac
  done

  if [[ "${#SELECTED_AGENT_OS[@]}" -eq 1 && "${SELECTED_AGENT_OS[0]}" == "$DEFAULT_AGENT_OS" && "${#loaded_agent_os[@]}" -gt 0 ]]; then
    SELECTED_AGENT_OS=("${loaded_agent_os[@]}")
  fi
  if [[ "${#SELECTED_AREAS[@]}" -eq 0 && "${#loaded_areas[@]}" -gt 0 ]]; then
    SELECTED_AREAS=("${loaded_areas[@]}")
  fi
  if [[ "${#SELECTED_SPECS[@]}" -eq 0 && "${#loaded_specs[@]}" -gt 0 ]]; then
    SELECTED_SPECS=("${loaded_specs[@]}")
  fi

  # Restore MCP integration selections so configure_*_if_needed skip interactive prompts
  local mcp_item
  if [[ "${#loaded_mcp_integrations[@]}" -gt 0 ]]; then
    for mcp_item in "${loaded_mcp_integrations[@]}"; do
      case "$mcp_item" in
        context7)
          if [[ -z "${AGENTIC_ENABLE_CONTEXT7:-}" ]]; then
            AGENTIC_ENABLE_CONTEXT7="y"
          fi
          ;;
        mempalace)
          if [[ -z "${AGENTIC_ENABLE_MEMPALACE:-}" ]]; then
            AGENTIC_ENABLE_MEMPALACE="y"
          fi
          ;;
      esac
    done
  fi

  if [[ -n "$loaded_telegram_enabled" ]]; then
    OPENCODE_TELEGRAM_ENABLED="$loaded_telegram_enabled"
    OPENCODE_TELEGRAM_BOT_TOKEN="$loaded_telegram_bot_token"
    OPENCODE_TELEGRAM_CHAT_ID="$loaded_telegram_chat_id"
    OPENCODE_PLUGINS_CONFIGURED=true
  fi
  if [[ -n "$loaded_mapper_enabled" ]]; then
    OPENCODE_AGENT_MODEL_MAPPER_ENABLED="$loaded_mapper_enabled"
    OPENCODE_PLUGINS_CONFIGURED=true
  fi
}

path_ref_for_shell_export() {
  local dir="$1"
  if [[ "$dir" == "$HOME/"* ]]; then
    printf '$HOME/%s\n' "${dir#"$HOME/"}"
    return
  fi
  printf '%s\n' "$dir"
}

profile_has_path_export() {
  local profile_file="$1"
  local dir="$2"
  [[ -f "$profile_file" ]] || return 1

  local path_ref
  path_ref="$(path_ref_for_shell_export "$dir")"

  if grep -Fq "$dir" "$profile_file"; then
    return 0
  fi

  grep -Fq "$path_ref" "$profile_file"
}

self_install_profile_file() {
  local shell_name
  shell_name="$(basename -- "${SHELL:-}")"
  case "$shell_name" in
    zsh)
      printf '%s\n' "$HOME/.zshrc"
      ;;
    bash)
      printf '%s\n' "$HOME/.bashrc"
      ;;
    *)
      printf '%s\n' "$HOME/.profile"
      ;;
  esac
}

ensure_bin_dir_in_shell_path() {
  local bin_dir="$1"
  local profile_file
  profile_file="$(self_install_profile_file)"

  local path_ref
  path_ref="$(path_ref_for_shell_export "$bin_dir")"
  local export_line="export PATH=\"$path_ref:\$PATH\""

  if profile_has_path_export "$profile_file" "$bin_dir"; then
    log "PATH hint already present in $profile_file"
    return
  fi

  if [[ "$DRY_RUN" == true ]]; then
    log "DRY-RUN append PATH export to $profile_file"
    log "DRY-RUN line: $export_line"
    return
  fi

  if [[ ! -e "$profile_file" ]]; then
    : > "$profile_file"
    unique_append "$profile_file" COPIED_PATHS
  fi

  {
    echo
    echo "# Added by $APP_NAME self-install on $(date +%Y-%m-%d)"
    echo "$export_line"
  } >> "$profile_file"
  log "Added PATH export to $profile_file"
}

copy_dir_contents() {
  local src="$1"
  local dest="$2"
  ensure_dir "$dest"
  if [[ "$DRY_RUN" == true ]]; then
    log "DRY-RUN copy managed contents $src -> $dest"
    unique_append "$dest" COPIED_PATHS
    return
  fi

  local event kind value events_file
  events_file="$(mktemp "${TMPDIR:-/tmp}/agentic-copy-events.XXXXXX")"
  python3 - "$src" "$dest" "$REPO_ROOT" "$PROJECT_DIR" "$(project_manifest_path)" "$APP_REPO_LINK" "$(app_version_label)" > "$events_file" <<'PY'
import hashlib
import json
import re
import sys
from pathlib import Path

src = Path(sys.argv[1])
dest_root = Path(sys.argv[2])
repo_root = Path(sys.argv[3])
project_dir = Path(sys.argv[4])
manifest = Path(sys.argv[5])
repo = sys.argv[6]
version = sys.argv[7]


def emit(kind: str, value: str) -> None:
    print(f"{kind}\t{value}")


def sha256(path: Path) -> str:
    h = hashlib.sha256()
    with path.open("rb") as fh:
        for chunk in iter(lambda: fh.read(1024 * 1024), b""):
            h.update(chunk)
    return h.hexdigest()


managed = None
if manifest.exists():
    managed = {}
    try:
        data = json.loads(manifest.read_text(encoding="utf-8"))
        for item in data.get("managed_files", []):
            rel = item.get("path")
            if rel:
                managed[rel] = item
    except Exception:
        managed = {}


def rel_to_project(path: Path) -> str:
    try:
        return str(path.relative_to(project_dir))
    except ValueError:
        return str(path)


def rel_to_repo(path: Path) -> str:
    try:
        return str(path.relative_to(repo_root))
    except ValueError:
        return str(path)


def yaml_quote(value: str) -> str:
    return json.dumps(value, ensure_ascii=False)


def existing_created_by(target: Path) -> str:
    if not target.exists():
        return version
    try:
        old = target.read_text(encoding="utf-8")
    except Exception:
        return version
    match = re.search(r"(?m)^  created_by:\s*(.+?)\s*$", old)
    if not match:
        return version
    return match.group(1).strip().strip('"')


def markdown_with_marker(body: str, source_ref: str, target: Path) -> str:
    created_by = existing_created_by(target)
    block = (
        "agentic:\n"
        "  generated_by: agentic\n"
        f"  source: {yaml_quote(source_ref)}\n"
        f"  repository: {yaml_quote(repo)}\n"
        f"  created_by: {yaml_quote(created_by)}\n"
        f"  updated_by: {yaml_quote(version)}\n"
    )
    if body.startswith("---\n"):
        end = body.find("\n---", 4)
        if end != -1:
            return body[: end + 1] + block + body[end + 1 :]
    return "---\n" + block + "---\n" + body


def commented(body: str, prefix: str, source_ref: str) -> str:
    marker = f"Generated by agentic; source: {source_ref}; repository: {repo}"
    line = f"{prefix} {marker}\n"
    if body.startswith("#!"):
        first, sep, rest = body.partition("\n")
        if sep:
            return first + sep + line + rest
    return line + body


def add_marker(file_path: Path, target: Path, source_ref: str) -> str:
    text = file_path.read_text(encoding="utf-8")
    suffix = target.suffix.lower()
    if suffix == ".md":
        return markdown_with_marker(text, source_ref, target)
    if suffix == ".json":
        data = json.loads(text)
        if not isinstance(data, dict):
            raise SystemExit(f"Cannot add agentic metadata to non-object JSON: {target}")
        return json.dumps(data, indent=2, ensure_ascii=False) + "\n"
    if suffix in {".ts", ".tsx", ".js", ".jsx", ".css"}:
        return commented(text, "//", source_ref)
    if suffix in {".sh", ".toml", ".py", ".yml", ".yaml"}:
        return commented(text, "#", source_ref)
    return commented(text, "#", source_ref)


emit("DIR", str(dest_root))
for file_path in sorted(p for p in src.rglob("*") if p.is_file()):
    rel = file_path.relative_to(src)
    target = dest_root / rel
    project_rel = rel_to_project(target)
    source_ref = rel_to_repo(file_path)

    if managed is not None:
        if project_rel not in managed:
            if target.exists():
                emit("WARN", f"Skipping unmanaged target on rerun: {project_rel}")
                emit("SKIP", project_rel)
                continue
        managed_item = managed.get(project_rel, {})
        if managed_item.get("marker") == "config":
            continue
        expected_hash = managed_item.get("content_hash", "")
        if target.exists() and expected_hash and sha256(target) != expected_hash:
            emit("WARN", f"Skipping user-modified managed file: {project_rel}")
            emit("SKIP", project_rel)
            continue

    output = add_marker(file_path, target, source_ref)
    target.parent.mkdir(parents=True, exist_ok=True)
    emit("DIR", str(target.parent))
    if target.exists():
        try:
            if target.read_text(encoding="utf-8") == output:
                digest = sha256(target)
                emit("RECORD", f"{project_rel}|{source_ref}|{digest}|internal")
                continue
        except UnicodeDecodeError:
            pass
    target.write_text(output, encoding="utf-8")
    digest = sha256(target)
    emit("RECORD", f"{project_rel}|{source_ref}|{digest}|internal")
    emit("COPIED", str(target))
PY
  while IFS= read -r event || [[ -n "$event" ]]; do
    kind="${event%%	*}"
    if [[ "$kind" == "$event" ]]; then
      value=""
    else
      value="${event#*	}"
    fi
    record_agentic_event "$kind" "$value"
  done < "$events_file"
  rm -f "$events_file"
}

write_generated_text_file() {
  local dest="$1"
  local source_ref="$2"
  local content="$3"

  if [[ "$DRY_RUN" == true ]]; then
    log "DRY-RUN write generated file $dest"
    unique_append "$dest" COPIED_PATHS
    return
  fi

  can_write_managed_file "$dest" || return 0
  ensure_dir "$(dirname -- "$dest")"

  local tmp
  tmp="$(mktemp "${TMPDIR:-/tmp}/agentic-generated.XXXXXX")"
  printf '%s' "$content" > "$tmp"
  write_file_with_agentic_marker "$tmp" "$dest" "$source_ref"
  rm -f "$tmp"
}

write_json_file_with_agentic_metadata() {
  local dest="$1"
  local source_ref="$2"
  local python_body="$3"

  if [[ "$DRY_RUN" == true ]]; then
    log "DRY-RUN update JSON managed file $dest"
    unique_append "$dest" COPIED_PATHS
    return
  fi

  can_write_managed_file "$dest" || return 0
  ensure_dir "$(dirname -- "$dest")"
  local write_status
  write_status="$(python3 - "$dest" "$source_ref" "$APP_REPO_LINK" "$CONTEXT7_API_KEY" "$python_body" "$(app_version_label)" <<'PY'
import json
import sys
from pathlib import Path

path = Path(sys.argv[1])
source_ref = sys.argv[2]
repo = sys.argv[3]
context7_api_key = sys.argv[4]
body = sys.argv[5]
version = sys.argv[6]

data = {}
created_by = version
if path.exists():
    try:
        data = json.loads(path.read_text(encoding="utf-8"))
        if isinstance(data, dict):
            created_by = data.get("_agentic", {}).get("created_by", version)
    except Exception:
        data = {}
if not isinstance(data, dict):
    data = {}

namespace = {
    "data": data,
    "context7_api_key": context7_api_key,
}
exec(body, namespace)
data = namespace["data"]
metadata = data.setdefault("_agentic", {})
metadata["generated_by"] = "agentic"
metadata["repository"] = repo
metadata["created_by"] = created_by
metadata["updated_by"] = version
output = json.dumps(data, indent=2, ensure_ascii=False) + "\n"
if path.exists():
    try:
        if path.read_text(encoding="utf-8") == output:
            print("unchanged")
            raise SystemExit(0)
    except UnicodeDecodeError:
        pass
path.write_text(json.dumps(data, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
print("written")
PY
)"
  if [[ "$write_status" == "unchanged" ]]; then
    register_managed_file "$dest" "$source_ref" "internal" false
  else
    register_managed_file "$dest" "$source_ref" "internal"
  fi
}

write_json_config_file() {
  local dest="$1"
  local source_ref="$2"
  local python_body="$3"

  if [[ "$DRY_RUN" == true ]]; then
    log "DRY-RUN update JSON config file $dest"
    unique_append "$dest" COPIED_PATHS
    return
  fi

  can_write_managed_file "$dest" || return 0
  ensure_dir "$(dirname -- "$dest")"
  local write_status
  write_status="$(python3 - "$dest" "$CONTEXT7_API_KEY" "$python_body" <<'PY'
import json
import sys
from pathlib import Path

path = Path(sys.argv[1])
context7_api_key = sys.argv[2]
body = sys.argv[3]

data = {}
if path.exists():
    try:
        data = json.loads(path.read_text(encoding="utf-8"))
    except Exception:
        data = {}
if not isinstance(data, dict):
    data = {}

namespace = {
    "data": data,
    "context7_api_key": context7_api_key,
}
exec(body, namespace)
data = namespace["data"]
output = json.dumps(data, indent=2, ensure_ascii=False) + "\n"
if path.exists():
    try:
        if path.read_text(encoding="utf-8") == output:
            print("unchanged")
            raise SystemExit(0)
    except UnicodeDecodeError:
        pass
path.write_text(output, encoding="utf-8")
print("written")
PY
)"
  if [[ "$write_status" == "unchanged" ]]; then
    register_managed_file "$dest" "$source_ref" "config" false
  else
    register_managed_file "$dest" "$source_ref" "config"
  fi
}

write_text_config_file() {
  local dest="$1"
  local source_ref="$2"
  local content="$3"

  if [[ "$DRY_RUN" == true ]]; then
    log "DRY-RUN write text config file $dest"
    unique_append "$dest" COPIED_PATHS
    return
  fi

  can_write_managed_file "$dest" || return 0
  ensure_dir "$(dirname -- "$dest")"

  local write_status
  write_status="$(python3 - "$dest" "$content" <<'PY'
import sys
from pathlib import Path

path = Path(sys.argv[1])
content = sys.argv[2]
if path.exists():
    try:
        if path.read_text(encoding="utf-8") == content:
            print("unchanged")
            raise SystemExit(0)
    except UnicodeDecodeError:
        pass
path.write_text(content, encoding="utf-8")
print("written")
PY
)"
  if [[ "$write_status" == "unchanged" ]]; then
    register_managed_file "$dest" "$source_ref" "config" false
  else
    register_managed_file "$dest" "$source_ref" "config"
  fi
}

write_context7_opencode_config() {
  local dest="$PROJECT_DIR/opencode.json"
  local body
  body='
mcp = data.setdefault("mcp", {})
context7 = {
    "type": "remote",
    "url": "https://mcp.context7.com/mcp",
    "enabled": True,
}
if context7_api_key:
    context7["headers"] = {"CONTEXT7_API_KEY": context7_api_key}
mcp["context7"] = context7
'
  write_json_config_file "$dest" "generated:context7-opencode-config" "$body"
}

write_context7_opencode_legacy_config() {
  local dest="$PROJECT_DIR/.opencode/opencode.json"
  local body
  body='
mcp = data.setdefault("mcp", {})
context7 = {
    "type": "remote",
    "url": "https://mcp.context7.com/mcp",
    "enabled": True,
}
if context7_api_key:
    context7["headers"] = {"CONTEXT7_API_KEY": context7_api_key}
mcp["context7"] = context7
'
  write_json_config_file "$dest" "generated:context7-opencode-legacy-config" "$body"
}

write_codex_features_config() {
  local dest="$PROJECT_DIR/.codex/config.toml"
  local body
  body="$(python3 - "$dest" <<'PY'
import re
import sys
from pathlib import Path

path = Path(sys.argv[1])
text = path.read_text(encoding="utf-8") if path.exists() else ""
features_re = re.compile(r"(?ms)^(\[features\]\n)(.*?)(?=^\[|\Z)")


def enable_memories(match):
    header = match.group(1)
    body = match.group(2)
    lines = body.splitlines(keepends=True)
    output = []
    found = False
    for line in lines:
        if re.match(r"\s*memories\s*=", line):
            output.append("memories = true" + ("\n" if line.endswith("\n") else ""))
            found = True
        else:
            output.append(line)
    if not found:
        trailing = re.search(r"\n*\Z", body).group(0)
        main = body[: len(body) - len(trailing)] if trailing else body
        if main and not main.endswith("\n"):
            main += "\n"
        return header + main + "memories = true\n" + trailing
    return header + "".join(output)


if features_re.search(text):
    print(features_re.sub(enable_memories, text, count=1), end="")
elif text.strip():
    print(text.rstrip() + "\n\n[features]\nmemories = true\n", end="")
else:
    print("[features]\nmemories = true\n", end="")
PY
)"
  write_text_config_file "$dest" "generated:codex-features-config" "$body"
}

write_context7_codex_config() {
  local dest="$PROJECT_DIR/.codex/config.toml"
  local body
  body="$(python3 - "$dest" "$CONTEXT7_API_KEY" <<'PY'
import re
import sys
from pathlib import Path

path = Path(sys.argv[1])
api_key = sys.argv[2]
text = path.read_text(encoding="utf-8") if path.exists() else ""
text = re.sub(r"(?ms)^\[mcp_servers\.context7\]\n.*?(?=^\[|\Z)", "", text).strip()
block = '[mcp_servers.context7]\nurl = "https://mcp.context7.com/mcp"\n'
if api_key:
    escaped = api_key.replace("\\", "\\\\").replace('"', '\\"')
    block += f'http_headers = {{ "CONTEXT7_API_KEY" = "{escaped}" }}\n'
if text:
    print(block + "\n" + text.rstrip() + "\n", end="")
else:
    print(block, end="")
PY
)"
  write_text_config_file "$dest" "generated:context7-codex-config" "$body"
}

write_context7_claude_config() {
  local dest="$PROJECT_DIR/.mcp.json"
  local body
  body='
mcp_servers = data.setdefault("mcpServers", {})
context7 = {
    "type": "http",
    "url": "https://mcp.context7.com/mcp",
}
if context7_api_key:
    context7["headers"] = {"CONTEXT7_API_KEY": context7_api_key}
mcp_servers["context7"] = context7
'
  write_json_config_file "$dest" "generated:context7-claude-config" "$body"
}

write_context7_cursor_config() {
  local dest="$PROJECT_DIR/.cursor/mcp.json"
  local body
  body='
mcp_servers = data.setdefault("mcpServers", {})
context7 = {
    "url": "https://mcp.context7.com/mcp",
}
if context7_api_key:
    context7["headers"] = {"CONTEXT7_API_KEY": context7_api_key}
mcp_servers["context7"] = context7
'
  write_json_config_file "$dest" "generated:context7-cursor-config" "$body"
}


write_context7_kilocode_config() {
  local dest="$PROJECT_DIR/.kilocode/mcp.json"
  local body
  body='
mcp_servers = data.setdefault("mcpServers", {})
context7 = {
    "url": "https://mcp.context7.com/mcp",
}
if context7_api_key:
    context7["headers"] = {"CONTEXT7_API_KEY": context7_api_key}
mcp_servers["context7"] = context7
'
  write_json_config_file "$dest" "generated:context7-kilocode-config" "$body"
}

write_context7_antigravity_config() {
  local dest="$HOME/.gemini/antigravity/mcp_config.json"
  local body
  body='
mcp_servers = data.setdefault("mcpServers", {})
context7 = {
    "url": "https://mcp.context7.com/mcp",
}
if context7_api_key:
    context7["headers"] = {"CONTEXT7_API_KEY": context7_api_key}
mcp_servers["context7"] = context7
'
  write_json_config_file "$dest" "generated:context7-antigravity-config" "$body"
}

write_context7_gemini_config() {
  local dest="$PROJECT_DIR/.gemini/settings.json"
  local body
  body='
mcp_servers = data.setdefault("mcpServers", {})
context7 = {
    "httpUrl": "https://mcp.context7.com/mcp",
}
if context7_api_key:
    context7["headers"] = {"CONTEXT7_API_KEY": context7_api_key}
mcp_servers["context7"] = context7
'
  write_json_config_file "$dest" "generated:context7-gemini-config" "$body"
}

print_context7_key_recommendation() {
  [[ -z "$CONTEXT7_API_KEY" ]] || return 0

  out "Context7 MCP configured without an API key."
}

configure_context7_key_interactive() {
  is_interactive_terminal || return 0

  local choice
  if fzf_available; then
    choice="$(choose_single_fzf "Context7 API key mode:" "Use without API key" "Enter CONTEXT7_API_KEY" || true)"
  else
    echo "Context7 API key mode:" >&2
    echo "  1) Use without API key" >&2
    echo "  2) Enter CONTEXT7_API_KEY" >&2
    local answer
    read -r -p "Select one (empty=1): " answer
    answer="$(trim "$answer")"
    case "$answer" in
      ""|1) choice="Use without API key" ;;
      2) choice="Enter CONTEXT7_API_KEY" ;;
      *) error "Invalid choice"; exit 1 ;;
    esac
  fi

  if [[ "$choice" == "Enter CONTEXT7_API_KEY" ]]; then
    CONTEXT7_API_KEY="$(prompt_text_interactive "CONTEXT7_API_KEY" "$CONTEXT7_API_KEY")"
  else
    CONTEXT7_API_KEY=""
  fi
}

write_mempalace_opencode_config() {
  local dest="$1"
  local body
  body='
mcp = data.setdefault("mcp", {})
mcp["mempalace"] = {"type": "local", "command": ["mempalace-mcp"]}
'
  write_json_config_file "$dest" "generated:mempalace-opencode-config" "$body"
}

write_mempalace_codex_config() {
  local dest="$PROJECT_DIR/.codex/config.toml"
  local body
  body="$(python3 - "$dest" <<'PYCODE'
import re
import pathlib
import sys

path = pathlib.Path(sys.argv[1])
text = path.read_text(encoding='utf-8') if path.exists() else ''
block = "[mcp_servers.mempalace]\ncommand = \"mempalace-mcp\"\n"
text = re.sub(r"(?ms)^\[mcp_servers\.mempalace\]\n.*?(?=^\[|\Z)", "", text).strip()
if text:
    print(text.rstrip() + "\n\n" + block, end="")
else:
    print(block, end="")
PYCODE
)"
  write_text_config_file "$dest" "generated:mempalace-codex-config" "$body"
}

write_mempalace_generic_json_config() {
  local dest="$1"
  local marker="$2"
  local body
  body='
servers = data.setdefault("mcpServers", {})
servers["mempalace"] = {"command": "mempalace-mcp"}
'
  write_json_config_file "$dest" "$marker" "$body"
}

print_mempalace_project_setup_instructions() {
  local project_wing
  project_wing="$(mempalace_project_wing)"
  log "Optional MemPalace project indexing instructions for target project: $PROJECT_DIR"
  out "1) Ensure Python is installed and available in PATH."
  out "2) Install MemPalace:"
  out "   pip install mempalace"
  out "3) Initialize the project memory taxonomy without LLM calls:"
  out "   echo \"N\" | mempalace init \"$PROJECT_DIR\" --yes --no-llm"
  out "4) Mine project knowledge into its isolated wing:"
  out "   mempalace mine \"$PROJECT_DIR\" --wing \"$project_wing\""
  if [[ -d "$PROJECT_DIR/docs" ]]; then
    out "5) Mine shared project docs into the cross-project docs wing:"
    out "   mempalace mine \"$PROJECT_DIR/docs\" --wing shared_docs"
    out "6) Verify in your IDE/agent that MemPalace MCP tools are connected."
  else
    out "5) Verify in your IDE/agent that MemPalace MCP tools are connected."
  fi
  out "Note: agentic uses --no-llm by default to keep MemPalace setup low-cost."
}

mempalace_sanitize_wing_name() {
  local raw="$1"
  local sanitized
  sanitized="$(printf '%s' "$raw" | tr '[:upper:]' '[:lower:]' | sed -E 's/[^a-z0-9]+/_/g; s/^_+//; s/_+$//; s/_+/_/g')"
  if [[ -z "$sanitized" ]]; then
    sanitized="project"
  fi
  printf '%s\n' "$sanitized"
}

mempalace_project_wing() {
  mempalace_sanitize_wing_name "$(basename "$PROJECT_DIR")"
}

mempalace_shared_docs_wing() {
  printf '%s\n' "shared_docs"
}

write_mempalace_ignore_file() {
  local dest="$PROJECT_DIR/.mempalaceignore"
  local content
  content='node_modules/
.venv/
venv/
dist/
logs/
build/
target/
coverage/
.ai/
.git/
.github/
.cursor/
.agent/
.opencode/
.claude/
.gemini/
.codex/
.idea/
*.csv
*.parquet
*.log
*.jsonl

data/
dumps/
tmp/
'

  if [[ -e "$dest" ]]; then
    log "MemPalace ignore file already exists: $dest"
    return 0
  fi

  write_text_config_file "$dest" "generated:mempalace-ignore" "$content"
}

warn_mempalace_failure_reason() {
  local output_file="$1"
  [[ -f "$output_file" ]] || return 0

  if grep -Fq "incompatible architecture" "$output_file" && grep -Fq "numpy" "$output_file"; then
    warn "MemPalace failed because Python/NumPy architecture is inconsistent. Reinstall MemPalace dependencies with the same architecture as the Python running 'mempalace'."
    warn "Typical fix: reinstall numpy/chromadb/mempalace in the active Python environment, or use a matching arm64/x86_64 Python. See the MemPalace log above for the exact Python path."
    return 0
  fi

  if grep -Fq "No LLM provider reachable" "$output_file"; then
    warn "MemPalace could not reach an LLM provider and continued heuristics-only; this is non-fatal unless a later dependency error appears."
  fi
}

warn_mempalace_pip_failure_reason() {
  local output_file="$1"
  [[ -f "$output_file" ]] || return 0

  local reason
  reason="$(sed -n '/[^[:space:]]/{p;q;}' "$output_file" 2>/dev/null || true)"
  if [[ -n "$reason" ]]; then
    warn "pip failure reason: $reason"
  else
    warn "pip failure output was empty; inspect the MemPalace pip install log for details."
  fi
}

mempalace_timeout_seconds() {
  local value="${AGENTIC_MEMPALACE_TIMEOUT_SECONDS:-60}"
  if [[ "$value" =~ ^[0-9]+$ ]] && (( value > 0 )); then
    printf '%s\n' "$value"
    return
  fi
  printf '%s\n' "60"
}

run_mempalace_command() {
  local label="$1"
  shift
  local output_file timeout_seconds child_pid elapsed status
  output_file="$(mktemp "${TMPDIR:-/tmp}/agentic-mempalace.XXXXXX")"
  timeout_seconds="$(mempalace_timeout_seconds)"

  "$@" >"$output_file" 2>&1 &
  child_pid=$!
  elapsed=0
  status=0
  while kill -0 "$child_pid" 2>/dev/null; do
    if (( elapsed >= timeout_seconds )); then
      pkill -TERM -P "$child_pid" 2>/dev/null || true
      kill "$child_pid" 2>/dev/null || true
      sleep 1
      pkill -KILL -P "$child_pid" 2>/dev/null || true
      kill -9 "$child_pid" 2>/dev/null || true
      wait "$child_pid" 2>/dev/null || true
      warn "Timed out after ${timeout_seconds}s: $* (log: $output_file)"
      log_file_block "$label" "$output_file"
      return 1
    fi
    sleep 1
    elapsed=$((elapsed + 1))
  done

  if wait "$child_pid"; then
    log "$label completed"
    log_file_block "$label" "$output_file"
    rm -f "$output_file"
    return 0
  fi
  status=$?

  warn "Failed: $* (exit $status, log: $output_file)"
  log_file_block "$label" "$output_file"
  warn_mempalace_failure_reason "$output_file"
  return 1
}

install_mempalace_with_pip() {
    local pip_bin="$1"
    local output_file status
    local py_bin venv_dir venv_python

    output_file="$(mktemp "${TMPDIR:-/tmp}/agentic-mempalace-pip.XXXXXX")"

    if $pip_bin install mempalace >"$output_file" 2>&1; then
        log "MemPalace package installed via '$pip_bin install mempalace'"
        log_file_block "MemPalace pip install" "$output_file"
        rm -f "$output_file"
        return 0
    else
        status=$?
    fi

    if grep -qi "externally-managed-environment" "$output_file"; then
        log "Detected PEP 668 externally-managed Python environment; retrying inside isolated venv"

        py_bin="$(command -v python3 || command -v python)"
        if [[ -z "$py_bin" ]]; then
            warn "python3/python executable not found"
            return 1
        fi

        venv_dir="${HOME}/.agentic/mempalace-venv"

        if [[ ! -d "$venv_dir" ]]; then
            mkdir -p "$(dirname "$venv_dir")"

            if ! "$py_bin" -m venv "$venv_dir" >>"$output_file" 2>&1; then
                warn "Unable to create virtual environment at $venv_dir"
                log_file_block "MemPalace pip install" "$output_file"
                return 1
            fi
        fi

        venv_python="$venv_dir/bin/python"

        if ! "$venv_python" -m pip install --upgrade pip setuptools wheel >>"$output_file" 2>&1; then
            warn "Unable to upgrade pip inside virtual environment"
            log_file_block "MemPalace pip install" "$output_file"
            return 1
        fi

        if ! "$venv_python" -m pip install --no-cache-dir --upgrade mempalace >>"$output_file" 2>&1; then
            warn "Unable to install mempalace inside virtual environment"
            log_file_block "MemPalace pip install" "$output_file"
            return 1
        fi

        local_bin_dir="$HOME/.local/bin"

        mkdir -p "$local_bin_dir"

        ln -sf "$venv_dir/bin/mempalace" "$local_bin_dir/mempalace"

        export PATH="$local_bin_dir:$venv_dir/bin:$PATH"

        shell_name="$(basename "${SHELL:-}")"

        if [[ "$shell_name" == "zsh" ]]; then
            grep -qxF 'export PATH="$HOME/.local/bin:$PATH"' "$HOME/.zshrc" 2>/dev/null || \
                echo 'export PATH="$HOME/.local/bin:$PATH"' >> "$HOME/.zshrc"
        else
            grep -qxF 'export PATH="$HOME/.local/bin:$PATH"' "$HOME/.bashrc" 2>/dev/null || \
                echo 'export PATH="$HOME/.local/bin:$PATH"' >> "$HOME/.bashrc"
        fi

        log "MemPalace installed successfully inside virtual environment: $venv_dir"
        log "MemPalace binary linked to: $local_bin_dir/mempalace"
        log_file_block "MemPalace pip install" "$output_file"

        rm -f "$output_file"
        return 0
    fi

    warn "Unable to auto-install mempalace via pip; continuing with manual setup instructions (exit $status, log: $output_file)"
    log_file_block "MemPalace pip install" "$output_file"
    warn_mempalace_pip_failure_reason "$output_file"

    return 1
}

mempalace_venv_dir() {
  printf '%s\n' "${AGENTIC_MEMPALACE_VENV:-$HOME/.venvs/mempalace}"
}

mempalace_bin_dir() {
  printf '%s\n' "${AGENTIC_MEMPALACE_BIN_DIR:-$HOME/.local/bin}"
}

python3_command() {
  if command -v python3 >/dev/null 2>&1; then
    printf '%s\n' "python3"
    return 0
  fi
  if command -v python >/dev/null 2>&1; then
    printf '%s\n' "python"
    return 0
  fi
  return 1
}

install_mempalace_managed() {
  local py_bin venv_dir bin_dir venv_python venv_mempalace

  py_bin="$(python3_command)" || return 1
  venv_dir="$(mempalace_venv_dir)"
  bin_dir="$(mempalace_bin_dir)"

  mkdir -p "$(dirname "$venv_dir")" "$bin_dir"

  if [[ ! -x "$venv_dir/bin/python" ]]; then
    "$py_bin" -m venv "$venv_dir" || return 1
  fi

  venv_python="$venv_dir/bin/python"
  venv_mempalace="$venv_dir/bin/mempalace"

  "$venv_python" -m pip install --upgrade pip setuptools wheel >/dev/null 2>&1 || return 1
  "$venv_python" -m pip install --upgrade --no-cache-dir mempalace >/dev/null 2>&1 || return 1

  [[ -x "$venv_mempalace" ]] || return 1

  ln -sf "$venv_mempalace" "$bin_dir/mempalace"

  if [[ -x "$venv_dir/bin/mempalace-mcp" ]]; then
    ln -sf "$venv_dir/bin/mempalace-mcp" "$bin_dir/mempalace-mcp"
  fi

  export PATH="$bin_dir:$PATH"

  command -v mempalace >/dev/null 2>&1
}

initialize_mempalace_project() {
  local step_prefix="$1"
  local project_wing shared_docs_wing
  project_wing="$(mempalace_project_wing)"
  shared_docs_wing="$(mempalace_shared_docs_wing)"
  log "$step_prefix [4/4] Initializing project memory at $PROJECT_DIR (wing: $project_wing)"
  if ! command -v mempalace >/dev/null 2>&1; then
    warn "mempalace command is unavailable after install; please run setup manually"
    print_mempalace_project_setup_instructions
    return 1
  fi

  if ! run_mempalace_command "MemPalace init" bash -lc "echo \"N\" | mempalace init \"$PROJECT_DIR\" --yes --no-llm"; then
    print_mempalace_project_setup_instructions
    return 1
  fi
  if ! run_mempalace_command "MemPalace mine project wing" mempalace mine "$PROJECT_DIR" --wing "$project_wing"; then
    print_mempalace_project_setup_instructions
    return 1
  fi
  if [[ -d "$PROJECT_DIR/docs" ]]; then
    if ! run_mempalace_command "MemPalace mine shared docs wing" mempalace mine "$PROJECT_DIR/docs" --wing "$shared_docs_wing"; then
      print_mempalace_project_setup_instructions
      return 1
    fi
  fi
  log "$step_prefix [4/4] Initialization step finished"
}

setup_mempalace_for_agentic() {
  local initialize_project="${1:-false}"
  local step_prefix="MemPalace setup"

  if [[ "${AGENTIC_MEMPALACE_SETUP:-}" == "skip" ]]; then
    log "$step_prefix skipped by AGENTIC_MEMPALACE_SETUP=skip"
    if command -v mempalace-mcp >/dev/null 2>&1; then
      return 0
    fi
    return 1
  fi

  log "$step_prefix [1/4] Checking Python availability"
  if ! command -v python3 >/dev/null 2>&1 && ! command -v python >/dev/null 2>&1; then
    warn "Python is not installed. Install Python 3 first, then run: pip install mempalace"
    warn "Install help: https://www.python.org/downloads/"
    print_mempalace_project_setup_instructions
    return 1
  fi
  log "$step_prefix [1/4] Python check passed"

  if [[ -z "${AGENTIC_TEST_SOURCE_AGENTIC:-}" ]] && command -v mempalace-mcp >/dev/null 2>&1; then
    if [[ "$initialize_project" != "true" ]] || command -v mempalace >/dev/null 2>&1; then
      log "$step_prefix [2/4] MemPalace binaries already available; skipping pip install"
      if [[ "$initialize_project" != "true" ]]; then
        log "$step_prefix [4/4] Project memory initialization skipped for selected agent target(s)"
        return 0
      fi
      initialize_mempalace_project "$step_prefix"
      return $?
    fi
  fi

  log "$step_prefix [2/4] Checking pip availability"
  local pip_bin
  if ! pip_bin="$(pip_command)"; then
    warn "pip is not available. Install pip for Python 3, then run: pip install mempalace"
    print_mempalace_project_setup_instructions
    return 1
  fi
  log "$step_prefix [2/4] pip check passed"

  log "$step_prefix [3/4] Installing mempalace package"
  if ! install_mempalace_with_pip "$pip_bin"; then
    print_mempalace_project_setup_instructions
    return 1
  fi

  if [[ "$initialize_project" != "true" ]]; then
    log "$step_prefix [4/4] Project memory initialization skipped for selected agent target(s)"
    return 0
  fi

  initialize_mempalace_project "$step_prefix"
}

configure_mempalace_if_needed() {
  if ! selected_agent_os_contains "opencode" \
    && ! selected_agent_os_contains "codex" \
    && ! selected_agent_os_contains "claude" \
    && ! selected_agent_os_contains "cursor" \
    && ! selected_agent_os_contains "gemini" \
    && ! selected_agent_os_contains "kilocode" \
    && ! selected_agent_os_contains "antigravity"; then
    return
  fi

  local enable_mempalace="N"
  if [[ -n "${AGENTIC_ENABLE_MEMPALACE:-}" ]]; then
    enable_mempalace="$(trim "${AGENTIC_ENABLE_MEMPALACE}")"
  elif is_interactive_terminal && [[ -z "${AGENTIC_TEST_SOURCE_AGENTIC:-}" ]]; then
    read -r -p "Enable MemPalace MCP memory integration? [y/N]: " enable_mempalace
    enable_mempalace="$(trim "${enable_mempalace:-n}")"
    if [[ -z "$enable_mempalace" ]]; then enable_mempalace="n"; fi
  fi
  if [[ "$enable_mempalace" =~ ^[Nn]$ ]]; then
    log "Skipped MemPalace MCP configuration"
    return
  fi

  write_mempalace_ignore_file

  local initialize_mempalace_project="true"
  local mempalace_setup_ok="true"
  setup_mempalace_for_agentic "$initialize_mempalace_project" || mempalace_setup_ok="false"

  if [[ "$mempalace_setup_ok" != "true" ]]; then
    if ! command -v mempalace-mcp >/dev/null 2>&1; then
      warn "mempalace-mcp is unavailable; install/repair MemPalace and re-run setup"
    fi
  else
    log "MemPalace MCP binary found: mempalace-mcp"
  fi

  if selected_agent_os_contains "opencode"; then
    write_mempalace_opencode_config "$PROJECT_DIR/opencode.json"
    write_mempalace_opencode_config "$PROJECT_DIR/.opencode/opencode.json"
  fi
  if selected_agent_os_contains "codex"; then write_mempalace_codex_config; fi
  if selected_agent_os_contains "claude"; then
    write_mempalace_generic_json_config "$PROJECT_DIR/.mcp.json" "generated:mempalace-claude-config"
  fi
  if selected_agent_os_contains "cursor"; then
    write_mempalace_generic_json_config "$PROJECT_DIR/.cursor/mcp.json" "generated:mempalace-cursor-config"
  fi
  if selected_agent_os_contains "gemini"; then
    write_mempalace_generic_json_config "$PROJECT_DIR/.gemini/settings.json" "generated:mempalace-gemini-config"
  fi
  if selected_agent_os_contains "kilocode"; then
    write_mempalace_generic_json_config "$PROJECT_DIR/.kilocode/mcp.json" "generated:mempalace-kilocode-config"
  fi
  if selected_agent_os_contains "antigravity"; then
    write_mempalace_generic_json_config "$HOME/.gemini/antigravity/mcp_config.json" "generated:mempalace-antigravity-config"
  fi
}

configure_context7_if_needed() {
  if ! selected_agent_os_contains "opencode" \
    && ! selected_agent_os_contains "codex" \
    && ! selected_agent_os_contains "claude" \
    && ! selected_agent_os_contains "cursor" \
    && ! selected_agent_os_contains "gemini" \
    && ! selected_agent_os_contains "kilocode" \
    && ! selected_agent_os_contains "antigravity"; then
    return
  fi

  local enable_context7="${AGENTIC_ENABLE_CONTEXT7:-}"
  if [[ -n "$enable_context7" ]]; then
    enable_context7="$(trim "$enable_context7")"
  fi

  if is_interactive_terminal; then
    if [[ -z "$enable_context7" ]]; then
      read -r -p "Enable Context7 MCP configuration? [y/N]: " enable_context7
      enable_context7="$(trim "$enable_context7")"
    fi
    if [[ ! "$enable_context7" =~ ^[Yy]$ ]]; then
      log "Context7 MCP configuration disabled"
      return
    fi
    configure_context7_key_interactive

  elif [[ -n "$enable_context7" ]]; then
    if [[ ! "$enable_context7" =~ ^[Yy]$ ]]; then
      log "Context7 MCP configuration disabled"
      return
    fi
  elif [[ -z "$CONTEXT7_API_KEY" ]]; then
    log "Context7 MCP configuration skipped; set CONTEXT7_API_KEY or use an interactive install to enable it"
    return
  fi

  if selected_agent_os_contains "opencode"; then
    write_context7_opencode_config
    write_context7_opencode_legacy_config
  fi

  if selected_agent_os_contains "codex"; then
    write_context7_codex_config
  fi

  if selected_agent_os_contains "claude"; then
    write_context7_claude_config
  fi

  if selected_agent_os_contains "cursor"; then
    write_context7_cursor_config
  fi

  if selected_agent_os_contains "gemini"; then
    write_context7_gemini_config
  fi

  if selected_agent_os_contains "kilocode"; then
    write_context7_kilocode_config
  fi

  if selected_agent_os_contains "antigravity"; then
    write_context7_antigravity_config
  fi

  print_context7_key_recommendation
}

write_default_opencode_plugin_config() {
  ensure_dir "$APP_CONFIG_DIR"
  OPENCODE_TELEGRAM_ENABLED="false"
  OPENCODE_TELEGRAM_BOT_TOKEN=""
  OPENCODE_TELEGRAM_CHAT_ID=""
  OPENCODE_AGENT_MODEL_MAPPER_ENABLED="false"
  OPENCODE_PLUGINS_CONFIGURED=true
  if [[ "$DRY_RUN" == true ]]; then
    log "DRY-RUN write disabled opencode plugin config to $OPENCODE_PLUGIN_CONFIG_FILE"
  else
    python3 - "$OPENCODE_PLUGIN_CONFIG_FILE" <<'PY'
import json
import sys
from pathlib import Path

path = Path(sys.argv[1])
path.write_text(json.dumps({
    "telegram": {"enabled": False},
    "agentModelMapper": {"enabled": False},
}, indent=2) + "\n", encoding="utf-8")
PY
  fi
}

load_opencode_plugin_config_globals() {
  [[ -f "$OPENCODE_PLUGIN_CONFIG_FILE" ]] || return 1
  local values=()
  readlines values < <(python3 - "$OPENCODE_PLUGIN_CONFIG_FILE" <<'PY'
import json
import sys
from pathlib import Path

try:
    data = json.loads(Path(sys.argv[1]).read_text(encoding="utf-8"))
except Exception:
    data = {}
telegram = data.get("telegram", {}) if isinstance(data, dict) else {}
mapper = data.get("agentModelMapper", {}) if isinstance(data, dict) else {}
print("true" if telegram.get("enabled") is True else "false")
print(telegram.get("botToken") or "")
print(telegram.get("chatId") or "")
print("true" if mapper.get("enabled") is True else "false")
PY
)
  OPENCODE_TELEGRAM_ENABLED="${values[0]:-false}"
  OPENCODE_TELEGRAM_BOT_TOKEN="${values[1]:-}"
  OPENCODE_TELEGRAM_CHAT_ID="${values[2]:-}"
  OPENCODE_AGENT_MODEL_MAPPER_ENABLED="${values[3]:-false}"
  OPENCODE_PLUGINS_CONFIGURED=true
}

configure_opencode_plugins_if_needed() {
  selected_agent_os_contains "opencode" || return 0

  if [[ "$DRY_RUN" == true ]]; then
    log "DRY-RUN configure optional opencode plugins"
    return
  fi

  ensure_python_available
  ensure_dir "$APP_CONFIG_DIR"

  # During manifest replay/re-install, keep current global plugin settings and avoid prompts.
  if [[ "$INSTALL_SETTINGS_REPLAY" == true && "$OPENCODE_PLUGINS_CONFIGURED" == true ]]; then
    log "OpenCode plugin settings loaded from .agentic.json"
    return
  fi
  if [[ "$INSTALL_SETTINGS_REPLAY" == true && -f "$OPENCODE_PLUGIN_CONFIG_FILE" ]]; then
    load_opencode_plugin_config_globals || true
    log "OpenCode plugin config already exists; keeping current settings"
    return
  fi

  if ! is_interactive_terminal; then
    if [[ ! -f "$OPENCODE_PLUGIN_CONFIG_FILE" ]]; then
      write_default_opencode_plugin_config
    else
      load_opencode_plugin_config_globals || true
    fi
    return
  fi

  local plugin_options=("telegram-notification" "agent-model-mapper")
  local selected_plugins=()
  local use_fzf_plugins=false
  if fzf_available; then
    use_fzf_plugins=true
  elif ensure_fzf_or_fallback; then
    use_fzf_plugins=true
  fi

  if [[ "$use_fzf_plugins" == true ]]; then
    readlines selected_plugins < <(choose_multi_fzf_strict "Select optional OpenCode plugin(s):" "${plugin_options[@]}")
  else
    local selected_plugins_output
    selected_plugins_output="$(choose_multi_by_index "Select optional OpenCode plugin(s):" "${plugin_options[@]}")"
    readlines selected_plugins <<< "$selected_plugins_output"
  fi

  local enable_telegram="n" enable_agent_model_mapper="n"
  local selected_plugin
  for selected_plugin in "${selected_plugins[@]}"; do
    selected_plugin="$(trim "$selected_plugin")"
    [[ -z "$selected_plugin" ]] && continue
    case "$selected_plugin" in
      telegram-notification|telegram-opencode-notifier) enable_telegram="y" ;;
      agent-model-mapper) enable_agent_model_mapper="y" ;;
    esac
  done

  if [[ "$enable_telegram" =~ ^[Yy]$ ]]; then
    OPENCODE_TELEGRAM_BOT_TOKEN="$(prompt_text_interactive "Telegram botToken" "$OPENCODE_TELEGRAM_BOT_TOKEN")"
    OPENCODE_TELEGRAM_CHAT_ID="$(prompt_text_interactive "Telegram chatId" "$OPENCODE_TELEGRAM_CHAT_ID")"
    if [[ -z "$OPENCODE_TELEGRAM_BOT_TOKEN" || -z "$OPENCODE_TELEGRAM_CHAT_ID" ]]; then
      warn "Telegram plugin credentials are incomplete; disabling telegram-notification"
      enable_telegram="n"
      OPENCODE_TELEGRAM_BOT_TOKEN=""
      OPENCODE_TELEGRAM_CHAT_ID=""
    else
      log "Telegram plugin enabled; credentials will be stored in project .agentic.json"
    fi
  fi
  OPENCODE_TELEGRAM_ENABLED=$([[ "$enable_telegram" =~ ^[Yy]$ ]] && echo "true" || echo "false")
  OPENCODE_AGENT_MODEL_MAPPER_ENABLED=$([[ "$enable_agent_model_mapper" =~ ^[Yy]$ ]] && echo "true" || echo "false")
  OPENCODE_PLUGINS_CONFIGURED=true

  python3 - "$OPENCODE_PLUGIN_CONFIG_FILE" "$enable_telegram" "$enable_agent_model_mapper" <<'PY'
import json
import sys
from pathlib import Path

path = Path(sys.argv[1])
enable_telegram = sys.argv[2].lower() == "y"
enable_mapper = sys.argv[3].lower() == "y"
data = {
    "telegram": {
        "enabled": enable_telegram,
    },
    "agentModelMapper": {
        "enabled": enable_mapper,
    },
}
path.write_text(json.dumps(data, indent=2) + "\n", encoding="utf-8")
PY

}

opencode_agent_model_mapper_config_enabled() {
  if [[ "$OPENCODE_AGENT_MODEL_MAPPER_ENABLED" == "true" ]]; then
    return 0
  fi
  if [[ "$OPENCODE_AGENT_MODEL_MAPPER_ENABLED" == "false" ]]; then
    return 1
  fi
  [[ -f "$OPENCODE_PLUGIN_CONFIG_FILE" ]] || return 1
  python3 - "$OPENCODE_PLUGIN_CONFIG_FILE" <<'PY'
import json
import sys
from pathlib import Path

try:
    data = json.loads(Path(sys.argv[1]).read_text(encoding="utf-8"))
except Exception:
    raise SystemExit(1)
raise SystemExit(0 if data.get("agentModelMapper", {}).get("enabled") is True else 1)
PY
}

opencode_mapper_read_roles() {
  local agents_dir="$PROJECT_DIR/.opencode/agents"
  [[ -d "$agents_dir" ]] || return 0
  python3 - "$agents_dir" <<'PY'
import sys
from pathlib import Path

agents_dir = Path(sys.argv[1])

def parse_frontmatter(text):
    if not text.startswith("---\n"):
        return {}
    end = text.find("\n---", 4)
    if end == -1:
        return {}
    result = {}
    for line in text[4:end].splitlines():
        if ":" not in line:
            continue
        key, value = line.split(":", 1)
        result[key.strip()] = value.strip().strip("'\"")
    return result

for path in sorted(agents_dir.glob("*.md")):
    frontmatter = parse_frontmatter(path.read_text(encoding="utf-8"))
    name = path.stem.replace("\t", " ")
    mode = (frontmatter.get("mode") or "subagent").replace("\t", " ")
    description = (frontmatter.get("description") or "OpenCode agent").replace("\t", " ")
    print(f"{name}\t{mode}\t{description}")
PY
}

opencode_mapper_discover_models() {
  local config_path="$HOME/.config/opencode/opencode.json"
  local auth_path="$HOME/.local/share/opencode/auth.json"
  local models_cache_path="$HOME/.cache/opencode/models.json"
  python3 - "$config_path" "$auth_path" "$models_cache_path" <<'PY'
import json
import sys
from pathlib import Path

fallback = ["opencode/minimax-m2.5-free"]
config_path = Path(sys.argv[1])
auth_path = Path(sys.argv[2])
models_cache_path = Path(sys.argv[3])
models = []

def add_model(model):
    if isinstance(model, str) and model.strip() and "/" in model:
        models.append(model.strip())

def collect_provider_models(data):
    """Extract models from provider.<name>.models dict keys."""
    providers = data.get("provider")
    if not isinstance(providers, dict):
        return
    for provider_name, provider_data in providers.items():
        if not isinstance(provider_data, dict):
            continue
        provider_models = provider_data.get("models")
        if not isinstance(provider_models, dict):
            continue
        for model_name in provider_models:
            if isinstance(model_name, str) and model_name.strip():
                add_model(f"{provider_name}/{model_name}")

def collect(value):
    if isinstance(value, list):
        for item in value:
            collect(item)
        return
    if not isinstance(value, dict):
        return
    for key, item in value.items():
        if key in {"model", "id"} and isinstance(item, str) and "/" in item:
            add_model(item)
        if key == "fallback" and isinstance(item, list):
            for model in item:
                add_model(model)
        collect(item)

def read_json(path):
    try:
        return json.loads(path.read_text(encoding="utf-8"))
    except Exception:
        return None

def is_deprecated(model_data):
    if not isinstance(model_data, dict):
        return False
    status = str(model_data.get("status", "")).lower()
    lifecycle = str(model_data.get("lifecycle", "")).lower()
    return (
        model_data.get("deprecated") is True
        or status in {"deprecated", "retired", "removed"}
        or lifecycle in {"deprecated", "retired", "removed"}
    )

def collect_authenticated_provider_models(auth_data, cache_data):
    if not isinstance(auth_data, dict) or not isinstance(cache_data, dict):
        return
    for provider_name, auth in auth_data.items():
        if not isinstance(provider_name, str) or not provider_name.strip():
            continue
        if auth in (None, False):
            continue
        provider_data = cache_data.get(provider_name)
        if not isinstance(provider_data, dict):
            continue
        provider_models = provider_data.get("models")
        if isinstance(provider_models, dict):
            for model_name, model_data in provider_models.items():
                if isinstance(model_name, str) and model_name.strip() and not is_deprecated(model_data):
                    add_model(f"{provider_name}/{model_name}")
        elif isinstance(provider_models, list):
            for item in provider_models:
                if isinstance(item, str):
                    add_model(f"{provider_name}/{item}")
                elif isinstance(item, dict) and not is_deprecated(item):
                    model_name = item.get("id") or item.get("name")
                    if isinstance(model_name, str) and model_name.strip():
                        add_model(f"{provider_name}/{model_name}")

try:
    data = json.loads(config_path.read_text(encoding="utf-8"))
    collect_provider_models(data)
    collect(data)
except Exception:
    pass

collect_authenticated_provider_models(read_json(auth_path), read_json(models_cache_path))

seen = set()
for model in models or fallback:
    model = model.strip()
    if model and model not in seen:
        seen.add(model)
        print(model)
PY
}

opencode_mapper_has_complete_mapping() {
  local roles_file="$1"
  local config_path="$PROJECT_DIR/.opencode/opencode.json"
  local state_path="$PROJECT_DIR/.opencode/agent-model-mapper.state.json"
  python3 - "$roles_file" "$config_path" "$state_path" <<'PY'
import json
import sys
from pathlib import Path

roles_file, config_path, state_path = map(Path, sys.argv[1:])
try:
    state = json.loads(state_path.read_text(encoding="utf-8"))
    config = json.loads(config_path.read_text(encoding="utf-8"))
except Exception:
    raise SystemExit(1)
if not state.get("configured"):
    raise SystemExit(1)
agents = config.get("agent")
if not isinstance(agents, dict):
    raise SystemExit(1)
roles = [line.split("\t", 1)[0] for line in roles_file.read_text(encoding="utf-8").splitlines() if line]
for role in roles:
    agent = agents.get(role)
    if not isinstance(agent, dict) or not str(agent.get("model", "")).strip():
        raise SystemExit(1)
raise SystemExit(0)
PY
}

choose_opencode_mapper_model() {
  local role_name="$1"
  local role_mode="$2"
  local role_description="$3"
  local kind="$4"
  shift 4
  local models=("$@")

  if [[ "${AGENTIC_AGENT_MODEL_MAPPER_NO_FZF:-}" != "1" ]] && fzf_available; then
    local selected selected_model fzf_status
    set +e
    selected="$(for i in "${!models[@]}"; do printf '%s\t%s\n' "$((i + 1))" "${models[$i]}"; done | fzf \
      --ansi \
      --border \
      --height=70% \
      --layout=reverse \
      --no-sort \
      --prompt "$role_name $kind> " \
      --header "Select $kind model for $role_name" \
      --with-nth=2..)"
    fzf_status=$?
    set -e
    if [[ "$fzf_status" -eq 0 && -n "$(trim "$selected")" ]]; then
      selected_model="${selected#*	}"
      local model
      for model in "${models[@]}"; do
        if [[ "$model" == "$selected_model" ]]; then
          printf '%s\n' "$selected_model"
          return 0
        fi
      done
    fi
  fi

  echo >&2
  echo "$role_name ($role_mode) - $role_description" >&2
  local i
  for i in "${!models[@]}"; do
    echo "  $((i + 1))) ${models[$i]}" >&2
  done
  local answer
  read -r -p "Select $kind model for $role_name [1]: " answer
  answer="$(trim "$answer")"
  if [[ -z "$answer" ]]; then
    printf '%s\n' "${models[0]}"
    return 0
  fi
  if [[ "$answer" =~ ^[0-9]+$ ]] && (( answer >= 1 && answer <= ${#models[@]} )); then
    printf '%s\n' "${models[$((answer - 1))]}"
    return 0
  fi
  local model
  for model in "${models[@]}"; do
    if [[ "$model" == "$answer" ]]; then
      printf '%s\n' "$answer"
      return 0
    fi
  done
  warn "Unknown model '$answer', using ${models[0]}"
  printf '%s\n' "${models[0]}"
}

write_opencode_agent_model_mapping() {
  local roles_file="$1"
  local mapping_file="$2"
  local config_path="$PROJECT_DIR/.opencode/opencode.json"
  local state_path="$PROJECT_DIR/.opencode/agent-model-mapper.state.json"

  python3 - "$roles_file" "$mapping_file" "$config_path" "$state_path" <<'PY'
import json
import sys
from pathlib import Path

roles_file, mapping_file, config_path, state_path = map(Path, sys.argv[1:])
roles = []
for line in roles_file.read_text(encoding="utf-8").splitlines():
    if not line:
        continue
    name, mode, description = (line.split("\t") + ["", "", ""])[:3]
    roles.append({"name": name, "mode": mode, "description": description})

mapping = {}
for line in mapping_file.read_text(encoding="utf-8").splitlines():
    if not line:
        continue
    name, model, fallback = (line.split("\t") + ["", "", ""])[:3]
    mapping[name] = {"model": model, "fallback": [fallback] if fallback and fallback != model else []}

try:
    data = json.loads(config_path.read_text(encoding="utf-8"))
except Exception:
    data = {}
if not isinstance(data, dict):
    data = {}
agents = data.setdefault("agent", {})
for role in roles:
    selected = mapping.get(role["name"])
    if not selected:
        continue
    current = agents.get(role["name"])
    if not isinstance(current, dict):
        current = {}
    current.update({
        "mode": current.get("mode") or role["mode"],
        "description": current.get("description") or role["description"],
        "model": selected["model"],
        "fallback": selected["fallback"],
    })
    agents[role["name"]] = current

config_path.write_text(json.dumps(data, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
state_path.write_text(json.dumps({
    "configured": True,
    "roles": [role["name"] for role in roles],
}, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
PY

  register_managed_file "$config_path" "generated:opencode-agent-model-mapper-config" "config"
  register_managed_file "$state_path" "generated:opencode-agent-model-mapper-state" "config"
}

confirm_opencode_agent_model_mapping() {
  local mapping_file="$1"
  if fzf_available; then
    local summary selected
    summary="$(python3 - "$mapping_file" <<'PY'
import sys
from pathlib import Path

for line in Path(sys.argv[1]).read_text(encoding="utf-8").splitlines():
    if not line:
        continue
    name, model, fallback = (line.split("\t") + ["", "", ""])[:3]
    print(f"{name}: main={model} fallback={fallback}")
PY
)"
    set +e
    selected="$(printf '%s\n' "Confirm" "Cancel" | fzf \
      --ansi \
      --border \
      --height=70% \
      --layout=reverse \
      --no-sort \
      --prompt "Save OpenCode model mapping? " \
      --header "$summary")"
    local fzf_status=$?
    set -e
    [[ "$fzf_status" -eq 0 ]] || return 1
    [[ "$selected" == "Confirm" ]]
    return
  fi

  out "agent-model-mapper selected mapping:"
  python3 - "$mapping_file" <<'PY'
import sys
from pathlib import Path

for line in Path(sys.argv[1]).read_text(encoding="utf-8").splitlines():
    if not line:
        continue
    name, model, fallback = (line.split("\t") + ["", "", ""])[:3]
    print(f"  - {name}: main={model} fallback={fallback}")
PY
  confirm_action_interactive "Write .opencode/opencode.json agent model mapping?"
}

configure_opencode_agent_model_mapper_if_needed() {
  selected_agent_os_contains "opencode" || return 0
  opencode_agent_model_mapper_config_enabled || return 0

  if ! is_interactive_terminal; then
    log "agent-model-mapper install-time setup skipped because no interactive terminal is available"
    return 0
  fi

  local config_path="$PROJECT_DIR/.opencode/opencode.json"
  local state_path="$PROJECT_DIR/.opencode/agent-model-mapper.state.json"
  can_write_managed_file "$config_path" || return 0
  if [[ -e "$state_path" ]]; then
    can_write_managed_file "$state_path" || return 0
  fi

  local roles_file models_file mapping_file
  roles_file="$(mktemp "${TMPDIR:-/tmp}/agentic-opencode-roles.XXXXXX")"
  models_file="$(mktemp "${TMPDIR:-/tmp}/agentic-opencode-models.XXXXXX")"
  mapping_file="$(mktemp "${TMPDIR:-/tmp}/agentic-opencode-mapping.XXXXXX")"
  opencode_mapper_read_roles > "$roles_file"

  if [[ ! -s "$roles_file" ]]; then
    log "agent-model-mapper: skipped because .opencode/agents/*.md was not found"
    rm -f "$roles_file" "$models_file" "$mapping_file"
    return 0
  fi

  if opencode_mapper_has_complete_mapping "$roles_file"; then
    log "agent-model-mapper: skipped because all Agentic roles already have model mappings"
    rm -f "$roles_file" "$models_file" "$mapping_file"
    return 0
  fi

  opencode_mapper_discover_models > "$models_file"
  local models=()
  readlines models < "$models_file"
  if [[ "${#models[@]}" -eq 0 ]]; then
    models=("opencode/minimax-m2.5-free")
  fi

  out "agent-model-mapper: choose OpenCode models for Agentic roles"
  local role_name role_mode role_description model fallback
  exec 3<&0
  while IFS=$'\t' read -r role_name role_mode role_description || [[ -n "${role_name:-}" ]]; do
    [[ -n "$role_name" ]] || continue
    model="$(choose_opencode_mapper_model "$role_name" "$role_mode" "$role_description" "main" "${models[@]}" <&3)"
    fallback="$(choose_opencode_mapper_model "$role_name" "$role_mode" "$role_description" "fallback" "${models[@]}" <&3)"
    printf '%s\t%s\t%s\n' "$role_name" "$model" "$fallback" >> "$mapping_file"
  done < "$roles_file"

  exec 3<&-
  if ! confirm_opencode_agent_model_mapping "$mapping_file"; then
    log "agent-model-mapper: skipped by user; no files changed"
    rm -f "$roles_file" "$models_file" "$mapping_file"
    return 0
  fi

  write_opencode_agent_model_mapping "$roles_file" "$mapping_file"
  log "agent-model-mapper: updated .opencode/opencode.json"
  rm -f "$roles_file" "$models_file" "$mapping_file"
}

normalize_selected_agent_os() {
  local normalized=()
  local agent
  if [[ "${#SELECTED_AGENT_OS[@]}" -eq 0 ]]; then
    SELECTED_AGENT_OS=("$DEFAULT_AGENT_OS")
    return
  fi
  for agent in "${SELECTED_AGENT_OS[@]}"; do
    agent="$(trim "$agent")"
    [[ -z "$agent" ]] && continue
    unique_append "$agent" normalized
  done
  if [[ "${#normalized[@]}" -eq 0 ]]; then
    normalized=("$DEFAULT_AGENT_OS")
  fi
  SELECTED_AGENT_OS=("${normalized[@]}")
}

copy_extension_for_agent() {
  local agent_os="$1"
  local project_dir="$2"

  if [[ "$agent_os" == "$DEFAULT_AGENT_OS" ]] || [[ "$agent_os" == "agents" ]]; then
    return
  fi

  local src="$EXTENSIONS_ROOT/$agent_os"
  local dest="$project_dir/.$agent_os"

  if [[ ! -d "$src" ]]; then
    warn "No extension directory found for '$agent_os' at $src (skipped)"
    return
  fi

  copy_dir_contents "$src" "$dest"
}

copy_extensions() {
  local project_dir="$1"
  local agent_os
  for agent_os in "${SELECTED_AGENT_OS[@]}"; do
    copy_extension_for_agent "$agent_os" "$project_dir"
  done
}

copy_specialization_assets() {
  local project_dir="$1"
  local spec_key

  for spec_key in "${SELECTED_SPECS[@]}"; do
    local area="${spec_key%%.*}"
    local spec="${spec_key#*.}"
    local src_root="$AREAS_ROOT/$area/$spec"

    if [[ ! -d "$src_root" ]]; then
      warn "Specialization path not found: $src_root"
      continue
    fi

    local bucket
    for bucket in "${INSTALL_DIRS[@]}"; do
      local src="$src_root/$bucket"
      if [[ ! -d "$src" ]]; then
        continue
      fi

      local targets=()
      local target
      for target in "${SELECTED_AGENT_OS[@]}"; do
        unique_append "$target" targets
      done
      unique_append "agents" targets

      local dest_dirs=()
      for target in "${targets[@]}"; do
        local dest_dir
        dest_dir="$(get_dest_dir "$target" "$bucket")"
        if [[ "$dest_dir" == "-" ]]; then
          continue
        fi
        unique_append "$dest_dir" dest_dirs
      done

      local resolved_dir
      for resolved_dir in "${dest_dirs[@]}"; do
        local dest="$project_dir/$resolved_dir"
        copy_dir_contents "$src" "$dest"
      done
    done
  done
}

build_header() {
  local out="$1"
  local rules_dir
  rules_dir="$(get_dest_dir "${SELECTED_AGENT_OS[0]}" "rules")"
  {
    echo "# Agentic Project Guidelines"
    echo
    echo "Generated by $SCRIPT_NAME."
    echo
    echo "## Installation Context"
    echo "- Agent OS targets: ${SELECTED_AGENT_OS[*]}"
    echo "- Primary agent rules directory: $rules_dir"
    echo "- Areas: ${SELECTED_AREAS[*]}"
    echo "- Specializations: ${SELECTED_SPECS[*]}"
    echo
    echo "---"
    echo
  } > "$out"
}

append_specialization_template() {
  local out="$1"
  local spec_key="$2"
  local area="${spec_key%%.*}"
  local spec="${spec_key#*.}"
  local src="$AREAS_ROOT/$area/$spec/AGENTS.md"

  {
    echo "## ${area}/${spec}"
    echo
    if [[ -f "$src" ]]; then
      cat "$src"
    else
      echo "No specialization AGENTS.md template found for ${spec_key}."
    fi
    echo
    echo "---"
    echo
  } >> "$out"
}

append_root_agents_template() {
  local out="$1"
  local src="$ROOT_AGENTS_FILE"

  {
    echo "## Shared guidance"
    echo
    if [[ -f "$src" ]]; then
      cat "$src"
    else
      echo "No root AGENTS.md template found."
    fi
    echo
    echo "---"
    echo
  } >> "$out"
}

generate_agents_md() {
  local project_dir="$1"
  local outputs=()

  if selected_agent_os_contains "opencode"; then
    unique_append "$project_dir/.opencode/AGENTS.md" outputs
  fi

  unique_append "$project_dir/AGENTS.md" outputs

  if [[ "$DRY_RUN" == true ]]; then
    local dry_run_out
    for dry_run_out in "${outputs[@]}"; do
      log "DRY-RUN generate $dry_run_out"
      unique_append "$dry_run_out" COPIED_PATHS
    done
    return
  fi

  ensure_dir "$project_dir"
  local tmp
  tmp="$(mktemp "${TMPDIR:-/tmp}/agentic-agents.XXXXXX")"
  build_header "$tmp"
  append_root_agents_template "$tmp"

  local spec_key
  for spec_key in "${SELECTED_SPECS[@]}"; do
    append_specialization_template "$tmp" "$spec_key"
  done

  local out
  for out in "${outputs[@]}"; do
    write_file_with_agentic_marker "$tmp" "$out" "generated:AGENTS.md"
  done
  rm -f "$tmp"
}

copy_memory_md() {
  local project_dir="$1"
  local src="$REPO_ROOT/MEMORY.md"

  if [[ ! -f "$src" ]]; then
    warn "MEMORY.md not found in knowledge base at $src; skipping"
    return
  fi

  local outputs=()

  if selected_agent_os_contains "opencode"; then
    unique_append "$project_dir/.opencode/MEMORY.md" outputs
  fi

  local needs_root=false
  local agent_os
  for agent_os in "${SELECTED_AGENT_OS[@]}"; do
    if [[ "$agent_os" != "opencode" ]]; then
      needs_root=true
      break
    fi
  done

  if [[ "$needs_root" == true ]] || ! selected_agent_os_contains "opencode"; then
    unique_append "$project_dir/MEMORY.md" outputs
  fi

  local out
  for out in "${outputs[@]}"; do
    write_file_with_agentic_marker "$src" "$out" "generated:MEMORY.md"
  done
}

validate_inputs() {
  local available_areas
  available_areas="$(list_areas || true)"

  if [[ -z "$PROJECT_DIR" ]]; then
    error "--project-dir is required"
    exit 1
  fi

  if [[ "${#SELECTED_AREAS[@]}" -eq 0 ]]; then
    error "--areas is required"
    exit 1
  fi

  if [[ "${#SELECTED_SPECS[@]}" -eq 0 ]]; then
    error "--specializations is required"
    exit 1
  fi

  local area
  for area in "${SELECTED_AREAS[@]}"; do
    if ! grep -qx "$area" <<< "$available_areas"; then
      error "unknown area '$area'"
      exit 1
    fi
  done

  local spec_key
  for spec_key in "${SELECTED_SPECS[@]}"; do
    if [[ "$spec_key" != *.* ]]; then
      error "specialization must be in area.spec format: $spec_key"
      exit 1
    fi

    local area_name="${spec_key%%.*}"
    local spec_name="${spec_key#*.}"
    if [[ ! -d "$AREAS_ROOT/$area_name/$spec_name" ]]; then
      error "specialization not found: $spec_key"
      exit 1
    fi

    local found=false
    local selected_area
    for selected_area in "${SELECTED_AREAS[@]}"; do
      if [[ "$selected_area" == "$area_name" ]]; then
        found=true
        break
      fi
    done
    if [[ "$found" == false ]]; then
      error "specialization '$spec_key' not included by selected areas"
      exit 1
    fi
  done

  local agentos_choices
  agentos_choices="$(get_agentos_choices)"
  local agent
  for agent in "${SELECTED_AGENT_OS[@]}"; do
    if ! grep -qx "$agent" <<< "$agentos_choices"; then
      error "unknown agent OS '$agent'"
      exit 1
    fi
  done
}

print_report() {
  write_changed_paths_report

  out
  out "=== Installation report ===" "$COLOR_HEADER"
  out "Agentic version: $(app_version_label)"
  out "Project dir: $PROJECT_DIR"
  out "Knowledge base repo: $REPO_ROOT"
  out "Config file: $APP_CONFIG_FILE"
  out "Agent OS targets: ${SELECTED_AGENT_OS[*]}"
  out "Areas: ${SELECTED_AREAS[*]}"
  out "Specializations: ${SELECTED_SPECS[*]}"

  out
  out "Created directories: ${#CREATED_PATHS[@]}"
  out "Copied/generated paths: ${#COPIED_PATHS[@]}"
  out "Changed paths report: $CHANGED_PATHS_REPORT_FILE"

  out
  out "Warnings:"
  if [[ "${#WARNINGS[@]}" -eq 0 ]]; then
    out "- (none)"
  else
    local warning
    for warning in "${WARNINGS[@]}"; do
      out "- $warning"
    done
  fi
}

detect_runtime_platform_label() {
  local platform
  platform="$(detect_platform)"
  if [[ "$platform" == "linux" ]] && grep -qiE "(microsoft|wsl)" /proc/version 2>/dev/null; then
    echo "wsl"
    return
  fi
  echo "$platform"
}

get_agent_binary_name() {
  local agent_os="$1"
  case "$agent_os" in
    codex) echo "codex" ;;
    claude) echo "claude" ;;
    opencode) echo "opencode" ;;
    cursor) echo "cursor-agent" ;;
    gemini) echo "gemini" ;;
    antigravity) echo "antigravity" ;;
    *) echo "" ;;
  esac
}

print_missing_agent_binary_guides() {
  local platform_label
  platform_label="$(detect_runtime_platform_label)"
  local missing_lines=()
  local agent_os

  for agent_os in "${SELECTED_AGENT_OS[@]}"; do
    local binary_name
    binary_name="$(get_agent_binary_name "$agent_os")"
    if [[ -z "$binary_name" ]]; then
      continue
    fi
    if command -v "$binary_name" >/dev/null 2>&1; then
      continue
    fi

    local install_link=""
    case "$agent_os" in
      codex) install_link="https://github.com/openai/codex" ;;
      claude) install_link="https://docs.anthropic.com/en/docs/claude-code/quickstart" ;;
      opencode) install_link="https://opencode.ai/docs" ;;
      cursor) install_link="https://docs.cursor.com/get-started/installation" ;;
      gemini) install_link="https://github.com/google-gemini/gemini-cli" ;;
      antigravity) install_link="https://github.com/getantigravity/antigravity" ;;
    esac

    missing_lines+=("- $agent_os: binary '$binary_name' is not installed on $platform_label.")
    if [[ -n "$install_link" ]]; then
      missing_lines+=("  Install guide: $install_link")
    fi
  done

  if [[ "${#missing_lines[@]}" -eq 0 ]]; then
    return
  fi

  out
  out "=== Agent binary setup recommendations ===" "$COLOR_HEADER"
  local missing_line
  for missing_line in "${missing_lines[@]}"; do
    out "$missing_line"
  done
}

changelog_file_path() {
  local candidate
  for candidate in "$SCRIPT_DIR/CHANGELOG.md" "$REPO_ROOT/CHANGELOG.md" "$APP_REPO_DIR/CHANGELOG.md"; do
    [[ -f "$candidate" ]] || continue
    printf '%s\n' "$candidate"
    return 0
  done
  return 1
}

print_current_changelog() {
  local changelog version
  changelog="$(changelog_file_path || true)"
  version="$(app_version_label)"
  if [[ -z "$changelog" ]]; then
    warn "CHANGELOG.md not found; skipping changelog output"
    return
  fi

  local section_file
  section_file="$(mktemp "${TMPDIR:-/tmp}/agentic-changelog.XXXXXX")"
  awk -v wanted="## $version" '
    $0 == wanted { in_section = 1; print; next }
    in_section && /^## / { exit }
    in_section { print }
  ' "$changelog" > "$section_file"

  if [[ ! -s "$section_file" ]]; then
    rm -f "$section_file"
    warn "No changelog section found for $version"
    return
  fi

  out
  out "=== Changelog $version ===" "$COLOR_HEADER"
  while IFS= read -r line || [[ -n "$line" ]]; do
    if [[ "$line" == "## $version" ]]; then
      continue
    fi
    out "$line"
  done < "$section_file"
  rm -f "$section_file"
}

doctor_agent_supported() {
  case "$1" in
    codex|opencode|claude|gemini) return 0 ;;
    *) return 1 ;;
  esac
}

doctor_enabled() {
  [[ "$DRY_RUN" != true ]] || return 1
  [[ "${AGENTIC_DOCTOR:-1}" != "0" ]] || return 1
  [[ "${AGENTIC_TEST_SOURCE_AGENTIC:-}" != "1" ]] || return 1
  return 0
}

doctor_prompt() {
  printf '%s\n' "Reply with exactly: AGENTIC_DOCTOR_OK"
}

doctor_prompt_for_agent() {
  doctor_prompt
}

doctor_smoke_label() {
  printf '%s\n' "lightweight smoke"
}

doctor_output_has_fatal_patterns() {
  local output_file="$1"
  grep -Eiq 'MCP.*(error|failed|failure|connection|connect|startup)|plugin.*(error|failed|failure)|auth.*(required|failed)|login required|permission.*(denied|required)|SyntaxError|Traceback|Invalid regular expression flags|An unexpected critical error occurred|FatalError|RuntimeError|EPERM|EACCES|panic:' "$output_file"
}

doctor_timeout_seconds() {
  local value="${AGENTIC_DOCTOR_TIMEOUT_SECONDS:-10}"
  if [[ ! "$value" =~ ^[0-9]+$ ]] || (( value < 1 )); then
    value=10
  fi
  printf '%s\n' "$value"
}

run_with_doctor_timeout() {
  local timeout_seconds="$1"
  shift

  "$@" &
  local child_pid=$!
  local elapsed=0
  local status=0
  while kill -0 "$child_pid" 2>/dev/null; do
    if (( elapsed >= timeout_seconds )); then
      pkill -TERM -P "$child_pid" 2>/dev/null || true
      kill "$child_pid" 2>/dev/null || true
      sleep 1
      pkill -KILL -P "$child_pid" 2>/dev/null || true
      kill -9 "$child_pid" 2>/dev/null || true
      wait "$child_pid" 2>/dev/null || true
      return 124
    fi
    sleep 1
    elapsed=$((elapsed + 1))
  done
  wait "$child_pid"
  status=$?
  return "$status"
}

doctor_copy_project() {
  local dest="$1"
  mkdir -p "$dest"
  if [[ -d "$PROJECT_DIR" ]]; then
    cp -R "$PROJECT_DIR/." "$dest/"
  fi
}

run_doctor_command() {
  local agent_os="$1"
  local work_dir="$2"
  local output_file="$3"
  local prompt
  prompt="$(doctor_prompt_for_agent "$agent_os")"

  case "$agent_os" in
    codex)
      codex exec --skip-git-repo-check --ephemeral --sandbox workspace-write -C "$work_dir" "$prompt" </dev/null >"$output_file" 2>&1
      ;;
    opencode)
      OPENCODE_DISABLE_AUTOUPDATE=1 opencode run --pure --dir "$work_dir" --dangerously-skip-permissions --format json --log-level ERROR "$prompt" >"$output_file" 2>&1
      ;;
    claude)
      (cd "$work_dir" && claude -p --permission-mode bypassPermissions --output-format stream-json "$prompt") >"$output_file" 2>&1
      ;;
    gemini)
      (cd "$work_dir" && gemini --prompt "$prompt") >"$output_file" 2>&1
      ;;
    *)
      return 2
      ;;
  esac
}

run_doctor_for_agent() {
  local agent_os="$1"
  local doctor_root="$2"
  local binary_name
  binary_name="$(get_agent_binary_name "$agent_os")"

  if [[ -z "$binary_name" ]] || ! command -v "$binary_name" >/dev/null 2>&1; then
    out "❌ $agent_os: binary '$binary_name' is not installed"
    return 1
  fi

  local work_dir output_file status timeout_seconds started_at elapsed smoke_label
  work_dir="$doctor_root/$agent_os"
  output_file="$doctor_root/$agent_os.log"
  timeout_seconds="$(doctor_timeout_seconds)"
  smoke_label="$(doctor_smoke_label "$agent_os")"
  doctor_copy_project "$work_dir"

  set +e
  started_at="$(date +%s)"
  run_with_doctor_timeout "$timeout_seconds" run_doctor_command "$agent_os" "$work_dir" "$output_file"
  status=$?
  elapsed=$(( $(date +%s) - started_at ))
  set -e

  log "$agent_os doctor finished: timeout=${timeout_seconds}s exit=$status elapsed=${elapsed}s"

  log_file_block "doctor $agent_os" "$output_file"

  if [[ "$status" -eq 124 || "$status" -eq 137 ]]; then
    out "❌ $agent_os: $smoke_label timed out after ${timeout_seconds}s (exit $status, elapsed ${elapsed}s, log: $output_file)"
    return 1
  fi

  if [[ "$status" -ne 0 ]]; then
    out "❌ $agent_os: $smoke_label failed (exit $status, elapsed ${elapsed}s, log: $output_file)"
    return 1
  fi

  if doctor_output_has_fatal_patterns "$output_file"; then
    out "❌ $agent_os: $smoke_label reported integration errors (exit $status, elapsed ${elapsed}s, log: $output_file)"
    return 1
  fi

  out "✅ $agent_os: $smoke_label passed (exit $status, elapsed ${elapsed}s)"
  return 0
}

run_agentic_doctor() {
  if ! doctor_enabled; then
    log "Agentic doctor skipped"
    return
  fi

  local selected_doctor_agents=()
  local agent_os
  for agent_os in "${SELECTED_AGENT_OS[@]}"; do
    if doctor_agent_supported "$agent_os"; then
      selected_doctor_agents+=("$agent_os")
    fi
  done

  if [[ "${#selected_doctor_agents[@]}" -eq 0 ]]; then
    log "Agentic doctor skipped: no supported real agentos selected"
    return
  fi

  local doctor_root
  doctor_root="$(mktemp -d "${TMPDIR:-/tmp}/agentic-doctor.XXXXXX")"
  out
  out "=== Agentic doctor ===" "$COLOR_HEADER"
  out "Doctor temp root: $doctor_root"
  out "Doctor timeout: $(doctor_timeout_seconds)s per agent"

  local failures=0
  for agent_os in "${selected_doctor_agents[@]}"; do
    if ! run_doctor_for_agent "$agent_os" "$doctor_root"; then
      failures=$((failures + 1))
    fi
  done

  if [[ "$AGENTIC_DOCTOR_KEEP_TMP" == "1" || "$failures" -gt 0 ]]; then
    out "Doctor temp root kept: $doctor_root"
  else
    rm -rf "$doctor_root"
  fi

  if [[ "$failures" -gt 0 ]]; then
    warn "Agentic doctor completed with $failures failing check(s)"
  else
    log "Agentic doctor completed successfully"
  fi
}

run_install() {
  init_run_logging
  ensure_repo_layout
  ensure_agentic_runtime_requirements
  normalize_selected_agent_os
  validate_inputs

  PROJECT_DIR="$(normalize_project_dir_path "$PROJECT_DIR")"
  ensure_dir "$PROJECT_DIR"
  configure_opencode_plugins_if_needed
  copy_extensions "$PROJECT_DIR"
  configure_opencode_agent_model_mapper_if_needed
  copy_specialization_assets "$PROJECT_DIR"
  generate_agents_md "$PROJECT_DIR"
  copy_memory_md "$PROJECT_DIR"
  if selected_agent_os_contains "codex"; then
    write_codex_features_config
  fi
  configure_context7_if_needed
  configure_mempalace_if_needed
  write_agentic_manifest "$PROJECT_DIR"
  print_report
  print_missing_agent_binary_guides
  print_current_changelog
  run_agentic_doctor
  out "Agentic log file: $RUN_LOG_FILE"
}

ascii_banner() {
  if [[ -n "$COLOR_HEADER" ]]; then
    printf '%s' "$COLOR_HEADER"
  fi
  cat <<'ART'
    _    ____ _____ _   _ _____ ___ ____
   / \  / ___| ____| \ | |_   _|_ _/ ___|
  / _ \| |  _|  _| |  \| | | |  | | |
 / ___ \ |_| | |___| |\  | | |  | | |___
/_/   \_\____|_____|_| \_| |_| |___\____|
ART
  if [[ -n "$COLOR_RESET" ]]; then
    printf '%s' "$COLOR_RESET"
  fi
}

prompt_with_default() {
  local prompt="$1"
  local default="$2"
  local answer
  read -r -p "$prompt [$default]: " answer
  answer="$(trim "$answer")"
  if [[ -z "$answer" ]]; then
    printf '%s\n' "$default"
  else
    printf '%s\n' "$answer"
  fi
}

prompt_with_default_fzf() {
  local prompt="$1"
  local default="$2"
  local fzf_args=(
    --ansi
    --border
    --height=70%
    --layout=reverse
    --cycle
    --no-sort
    --phony
    --print-query
    --bind "enter:accept"
    --prompt "$prompt "
    --header "Type path and press Enter to confirm (empty = $default)"
  )
  if [[ "${#FZF_COLOR_ARGS[@]}" -gt 0 ]]; then
    fzf_args+=("${FZF_COLOR_ARGS[@]}")
  fi

  local output
  # Keep one selectable row so Enter confirms query instead of canceling on 0/0.
  if ! output="$(printf '%s\n' "<press Enter to confirm path>" | fzf "${fzf_args[@]}")"; then
    error "Directory input canceled"
    exit 1
  fi

  local selected
  selected="$(printf '%s\n' "$output" | sed -n '1p')"
  selected="$(trim "$selected")"
  if [[ -n "$selected" ]]; then
    printf '%s\n' "$selected"
    return
  fi
  printf '%s\n' "$default"
}

prompt_text_fzf() {
  local prompt="$1"
  local default="${2:-}"
  local header="Type value and press Enter to confirm"
  if [[ -n "$default" ]]; then
    header="$header (empty = current value)"
  fi
  local fzf_args=(
    --ansi
    --border
    --height=70%
    --layout=reverse
    --cycle
    --no-sort
    --phony
    --print-query
    --bind "enter:accept"
    --prompt "$prompt "
    --header "$header"
  )
  if [[ "${#FZF_COLOR_ARGS[@]}" -gt 0 ]]; then
    fzf_args+=("${FZF_COLOR_ARGS[@]}")
  fi

  local output selected
  if ! output="$(printf '%s\n' "<press Enter to confirm>" | fzf "${fzf_args[@]}")"; then
    return 1
  fi
  selected="$(printf '%s\n' "$output" | sed -n '1p')"
  selected="$(trim "$selected")"
  if [[ -n "$selected" ]]; then
    printf '%s\n' "$selected"
  else
    printf '%s\n' "$default"
  fi
}

prompt_text_tui() {
  local prompt="$1"
  local default="${2:-}"
  local answer
  if [[ -n "$default" ]]; then
    read -r -p "$prompt [keep current]: " answer
  else
    read -r -p "$prompt: " answer
  fi
  answer="$(trim "$answer")"
  if [[ -n "$answer" ]]; then
    printf '%s\n' "$answer"
  else
    printf '%s\n' "$default"
  fi
}

prompt_text_interactive() {
  local prompt="$1"
  local default="${2:-}"
  if fzf_available; then
    prompt_text_fzf "$prompt" "$default"
  else
    prompt_text_tui "$prompt" "$default"
  fi
}

confirm_action_interactive() {
  local prompt="$1"
  local selected
  if fzf_available; then
    selected="$(choose_single_fzf "$prompt" "Confirm" "Cancel" || true)"
    [[ "$selected" == "Confirm" ]]
    return
  fi
  local answer
  read -r -p "$prompt [y/N]: " answer
  answer="$(trim "$answer")"
  [[ "$answer" =~ ^[Yy]([Ee][Ss])?$ ]]
}

choose_single_by_index() {
  local prompt="$1"
  shift
  local options=("$@")
  local i
  echo "$prompt" >&2
  for i in "${!options[@]}"; do
    echo "  $((i + 1))) ${options[$i]}" >&2
  done
  local answer
  read -r -p "Select one (empty=1): " answer
  answer="$(trim "$answer")"
  if [[ -z "$answer" ]]; then
    printf '%s\n' "${options[0]}"
    return
  fi
  if [[ ! "$answer" =~ ^[0-9]+$ ]] || (( answer < 1 || answer > ${#options[@]} )); then
    error "Invalid choice"
    exit 1
  fi
  printf '%s\n' "${options[$((answer - 1))]}"
}

choose_multi_by_index() {
  local prompt="$1"
  shift
  local options=("$@")
  local i
  echo "$prompt" >&2
  for i in "${!options[@]}"; do
    echo "  $((i + 1))) ${options[$i]}" >&2
  done
  local answer
  read -r -p "Select one or more (comma-separated indexes): " answer
  answer="$(trim "$answer")"
  if [[ -z "$answer" ]]; then
    echo ""
    return
  fi

  local out=()
  local idx
  local indexes=()
  IFS=',' read -r -a indexes <<< "$answer"
  for idx in "${indexes[@]}"; do
    idx="$(trim "$idx")"
    if [[ ! "$idx" =~ ^[0-9]+$ ]] || (( idx < 1 || idx > ${#options[@]} )); then
      error "Invalid selection index: $idx"
      exit 1
    fi
    unique_append "${options[$((idx - 1))]}" out
  done

  printf '%s\n' "${out[@]}"
}

fzf_available() {
  command -v fzf >/dev/null 2>&1
}

run_with_sudo_if_needed() {
  if (( EUID == 0 )); then
    "$@"
    return
  fi

  if command -v sudo >/dev/null 2>&1; then
    sudo "$@"
    return
  fi

  "$@"
}

auto_install_fzf_linux() {
  if command -v apt-get >/dev/null 2>&1; then
    run_with_sudo_if_needed apt-get update
    run_with_sudo_if_needed apt-get install -y fzf
    return 0
  fi
  if command -v dnf >/dev/null 2>&1; then
    run_with_sudo_if_needed dnf install -y fzf
    return 0
  fi
  if command -v yum >/dev/null 2>&1; then
    run_with_sudo_if_needed yum install -y fzf
    return 0
  fi
  if command -v pacman >/dev/null 2>&1; then
    run_with_sudo_if_needed pacman -Sy --noconfirm fzf
    return 0
  fi
  if command -v zypper >/dev/null 2>&1; then
    run_with_sudo_if_needed zypper --non-interactive install fzf
    return 0
  fi
  if command -v apk >/dev/null 2>&1; then
    run_with_sudo_if_needed apk add --no-cache fzf
    return 0
  fi
  return 1
}

auto_install_fzf_windows() {
  if command -v winget >/dev/null 2>&1; then
    winget install --id junegunn.fzf -e --accept-source-agreements --accept-package-agreements
    return 0
  fi
  if command -v choco >/dev/null 2>&1; then
    choco install fzf -y
    return 0
  fi
  if command -v scoop >/dev/null 2>&1; then
    scoop install fzf
    return 0
  fi
  return 1
}

auto_install_fzf_macos() {
  if command -v brew >/dev/null 2>&1; then
    brew install fzf
    return 0
  fi
  return 1
}

auto_install_fzf() {
  local platform
  platform="$(detect_platform)"

  case "$platform" in
    linux)
      auto_install_fzf_linux
      ;;
    windows)
      auto_install_fzf_windows
      ;;
    macos)
      auto_install_fzf_macos
      ;;
    *)
      return 1
      ;;
  esac
}

self_install_fzf_optional() {
  if [[ "$SELF_INSTALL_WITH_FZF" != true ]]; then
    return 0
  fi

  if [[ "$DRY_RUN" == true ]]; then
    log "DRY-RUN would try to auto-install fzf for platform: $(detect_platform)"
    return 0
  fi

  if fzf_available; then
    log "fzf already available"
    return 0
  fi

  log "Trying to install fzf for platform: $(detect_platform)"
  if auto_install_fzf && fzf_available; then
    log "fzf installed successfully"
    return 0
  fi

  warn "Could not auto-install fzf. TUI will use index-based fallback menus."
  return 0
}

ensure_fzf_or_fallback() {
  if fzf_available; then
    return 0
  fi

  warn "fzf is not installed. Falling back to index menus unless auto-install succeeds."

  if ! is_interactive_terminal; then
    return 1
  fi

  local answer
  read -r -p "Install fzf automatically now? [Y/n]: " answer
  answer="$(trim "${answer:-}")"
  if [[ -z "$answer" ]] || [[ "$answer" =~ ^[Yy]$ ]]; then
    if auto_install_fzf && fzf_available; then
      log "fzf installed successfully"
      return 0
    fi
    warn "Automatic fzf installation failed or fzf still unavailable."
    return 1
  fi

  warn "User declined automatic fzf installation."
  return 1
}

choose_single_fzf() {
  local prompt="$1"
  shift
  local options=("$@")

  if [[ "${#options[@]}" -eq 0 ]]; then
    return
  fi

  local fzf_args=(
    --ansi
    --border
    --height=70%
    --layout=reverse
    --cycle
    --no-sort
    --prompt "$prompt "
    --header "Use ↑/↓ to navigate • Enter to select"
  )
  if [[ "${#FZF_COLOR_ARGS[@]}" -gt 0 ]]; then
    fzf_args+=("${FZF_COLOR_ARGS[@]}")
  fi

  printf '%s\n' "${options[@]}" | fzf "${fzf_args[@]}"
}


choose_multi_fzf_strict() {
  local prompt="$1"
  shift
  local options=("$@")

  if [[ "${#options[@]}" -eq 0 ]]; then
    return
  fi

  local sentinel="<none>"
  local picked=()
  readlines picked < <(choose_multi_fzf "$prompt" "$sentinel" "${options[@]}")

  local item
  for item in "${picked[@]}"; do
    item="$(trim "$item")"
    [[ -z "$item" || "$item" == "$sentinel" ]] && continue
    printf '%s\n' "$item"
  done
}

choose_multi_fzf() {
  local prompt="$1"
  shift
  local options=("$@")

  if [[ "${#options[@]}" -eq 0 ]]; then
    return
  fi

  local fzf_args=(
    --ansi
    --border
    --height=75%
    --layout=reverse
    --cycle
    --no-sort
    --multi
    --bind "space:toggle"
    --bind "tab:toggle+down"
    --prompt "$prompt "
    --header "Use ↑/↓ to navigate • Space to select • Enter to confirm"
  )
  if [[ "${#FZF_COLOR_ARGS[@]}" -gt 0 ]]; then
    fzf_args+=("${FZF_COLOR_ARGS[@]}")
  fi

  printf '%s\n' "${options[@]}" | fzf "${fzf_args[@]}"
}

pick_theme_if_needed() {
  if [[ "$THEME_EXPLICIT" == true ]] || [[ "$THEME_LOADED_FROM_CONFIG" == true ]]; then
    return
  fi

  local selected
  if fzf_available; then
    selected="$(choose_single_fzf "Select interface theme:" "${THEME_CHOICES[@]}")"
  else
    selected="$(choose_single_by_index "Select interface theme:" "${THEME_CHOICES[@]}")"
  fi

  selected="$(trim "$selected")"
  if [[ -n "$selected" ]]; then
    THEME="$selected"
  fi
}

run_tui() {
  ensure_repo_layout

  if ! is_interactive_terminal; then
    error "TUI mode requires an interactive terminal"
    exit 1
  fi

  ensure_agentic_runtime_requirements

  pick_theme_if_needed
  set_theme_colors

  ascii_banner
  echo "${COLOR_HEADER}$APP_TUI_TITLE $(app_version_label)${COLOR_RESET}"
  echo "${COLOR_DIM}Theme: $THEME (resolved: $ACTIVE_THEME)${COLOR_RESET}"
  echo

  local use_fzf=false
  if fzf_available; then
    use_fzf=true
  elif ensure_fzf_or_fallback; then
    use_fzf=true
    # Re-apply color profile in case auto-install affected terminal capabilities.
    set_theme_colors
  fi

  if [[ "$use_fzf" == true ]]; then
    PROJECT_DIR="$(prompt_with_default_fzf "Target project directory [/tmp/agentic-project]:" "/tmp/agentic-project")"
  else
    PROJECT_DIR="$(prompt_with_default "Target project directory" "/tmp/agentic-project")"
  fi

  local agentos_choices=()
  readlines agentos_choices < <(get_agentos_choices)

  local picked_agent_os=()
  if [[ "$use_fzf" == true ]]; then
    readlines picked_agent_os < <(choose_multi_fzf "Select Agent OS target(s):" "${agentos_choices[@]}")
  else
    local picked_agent_os_output
    picked_agent_os_output="$(choose_multi_by_index "Select Agent OS target(s):" "${agentos_choices[@]}")"
    readlines picked_agent_os <<< "$picked_agent_os_output"
  fi
  if [[ "${#picked_agent_os[@]}" -eq 0 ]]; then
    SELECTED_AGENT_OS=("$DEFAULT_AGENT_OS")
  else
    SELECTED_AGENT_OS=("${picked_agent_os[@]}")
  fi

  local mcp_options=("context7" "mempalace")
  local picked_mcps=()
  if [[ "$use_fzf" == true ]]; then
    readlines picked_mcps < <(choose_multi_fzf_strict "Select optional MCP integration(s):" "${mcp_options[@]}")
  else
    local picked_mcps_output
    picked_mcps_output="$(choose_multi_by_index "Select optional MCP integration(s):" "<none>" "${mcp_options[@]}")"
    readlines picked_mcps <<< "$picked_mcps_output"
  fi

  AGENTIC_ENABLE_CONTEXT7="n"
  AGENTIC_ENABLE_MEMPALACE="n"
  local picked_mcp
  for picked_mcp in "${picked_mcps[@]}"; do
    case "$picked_mcp" in
      context7) AGENTIC_ENABLE_CONTEXT7="y" ;;
      mempalace) AGENTIC_ENABLE_MEMPALACE="y" ;;
    esac
  done

  local areas=()
  readlines areas < <(list_areas)

  local picked_areas=()
  if [[ "$use_fzf" == true ]]; then
    readlines picked_areas < <(choose_multi_fzf "Select area(s):" "${areas[@]}")
  else
    local picked_areas_output
    picked_areas_output="$(choose_multi_by_index "Select area(s):" "${areas[@]}")"
    readlines picked_areas <<< "$picked_areas_output"
  fi

  if [[ "${#picked_areas[@]}" -eq 0 ]]; then
    SELECTED_AREAS=(software)
  else
    SELECTED_AREAS=("${picked_areas[@]}")
  fi

  SELECTED_SPECS=()
  local area
  for area in "${SELECTED_AREAS[@]}"; do
    local specs=()
    readlines specs < <(list_specs "$area")

    local chosen_specs=()
    if [[ "$use_fzf" == true ]]; then
      readlines chosen_specs < <(choose_multi_fzf "Select specialization(s) for '$area':" "${specs[@]}")
    else
      local chosen_specs_output
      chosen_specs_output="$(choose_multi_by_index "Select specialization(s) for '$area':" "${specs[@]}")"
      readlines chosen_specs <<< "$chosen_specs_output"
    fi

    if [[ "${#chosen_specs[@]}" -eq 0 ]]; then
      error "No specialization selected for $area"
      exit 1
    fi

    local spec
    for spec in "${chosen_specs[@]}"; do
      SELECTED_SPECS+=("$area.$spec")
    done
  done

  save_user_config
  run_install
}

self_install() {
  local source_path="$SCRIPT_SOURCE"
  if [[ ! -r "$source_path" ]]; then
    error "Cannot read installer source at '$source_path'."
    error "Tip: download to a local file, then run self-install from that file."
    exit 1
  fi

  local bin_dir="$SELF_INSTALL_BIN_DIR"
  if [[ "$bin_dir" == "~/.local/bin" ]]; then
    bin_dir="${HOME}/.local/bin"
  fi

  local target="$bin_dir/$SELF_INSTALL_NAME"

  ensure_dir "$bin_dir"

  if [[ -e "$source_path" && -e "$target" ]]; then
    local source_real target_real
    source_real="$(cd -- "$(dirname -- "$source_path")" && pwd -P)/$(basename -- "$source_path")"
    target_real="$(cd -- "$(dirname -- "$target")" && pwd -P)/$(basename -- "$target")"
    if [[ "$source_real" == "$target_real" ]]; then
      if [[ -e "$APP_REPO_DIR/agentic" ]]; then
        source_path="$APP_REPO_DIR/agentic"
      else
        log "Source and target are already the same file: $target"
        log "Nothing to copy. Run '$APP_NAME upgrade' first to refresh the knowledge base checkout, or run self-install from a checkout."
        self_install_fzf_optional
        return
      fi
    fi
  fi

  if [[ -e "$target" ]] && [[ "$SELF_INSTALL_FORCE" != true ]]; then
    error "Target already exists: $target"
    error "Use --force to overwrite"
    exit 1
  fi

  if [[ "$DRY_RUN" == true ]]; then
    log "DRY-RUN install script to $target"
  else
    cp "$source_path" "$target"
    chmod +x "$target"
    unique_append "$target" COPIED_PATHS
    log "Installed: $target"
  fi

  self_install_fzf_optional

  case ":$PATH:" in
    *":$bin_dir:"*)
      log "PATH already includes $bin_dir"
      ;;
    *)
      warn "PATH does not include $bin_dir"
      ensure_bin_dir_in_shell_path "$bin_dir"
      warn "Open a new terminal or run: export PATH=\"$(path_ref_for_shell_export "$bin_dir"):\$PATH\""
      ;;
  esac

  echo
  echo "${COLOR_HEADER}=== Self-install report ===${COLOR_RESET}"
  echo "Source: $source_path"
  echo "Target binary: $target"
  echo "Config directory: $APP_CONFIG_DIR"
  echo "Knowledge base repo: $APP_REPO_DIR"
  echo "Install fzf requested: $SELF_INSTALL_WITH_FZF"
  echo "Dry-run: $DRY_RUN"
}

update_installed_binary_from_repo() {
  local source_path="$APP_REPO_DIR/agentic"
  local target="$SCRIPT_SOURCE"

  if [[ ! -r "$source_path" ]]; then
    warn "Cannot self-update installed binary; source not found: $source_path"
    return
  fi

  if [[ "$DRY_RUN" == true ]]; then
    log "DRY-RUN self-update installed binary $target from $source_path"
    return
  fi

  if cmp -s "$source_path" "$target"; then
    log "Installed binary already up to date: $target"
    return
  fi

  local target_dir target_tmp
  target_dir="$(dirname "$target")"
  target_tmp="$(mktemp "$target_dir/.agentic-update.XXXXXX")"

  cp "$source_path" "$target_tmp"
  chmod +x "$target_tmp"
  mv -f "$target_tmp" "$target"
  log "Updated installed binary: $target"
}

upgrade_repo_checkout() {
  refresh_repo_paths

  if resolve_dev_repo_root >/dev/null 2>&1; then
    ensure_git_available
    log "Updating development checkout at $REPO_ROOT"
    if [[ "$DRY_RUN" == true ]]; then
      log "DRY-RUN git -C $REPO_ROOT pull --ff-only"
      return
    fi
    git -C "$REPO_ROOT" pull --ff-only
    return
  fi

  if ! resolve_data_repo_root >/dev/null 2>&1; then
    log "Knowledge base checkout not found; bootstrapping initial clone first"
    ensure_repo_checkout
    return
  fi

  ensure_git_available
  log "Updating installed knowledge base checkout at $APP_REPO_DIR"
  if [[ "$DRY_RUN" == true ]]; then
    log "DRY-RUN git -C $APP_REPO_DIR pull --ff-only"
    return
  fi
  git -C "$APP_REPO_DIR" pull --ff-only
  refresh_repo_paths
  ensure_repo_layout
  update_installed_binary_from_repo
}

sync_current_project_after_upgrade() {
  local manifest="$PWD/$PROJECT_MANIFEST_NAME"
  if [[ ! -f "$manifest" ]]; then
    log "No $PROJECT_MANIFEST_NAME in current directory; knowledge base upgrade complete"
    return
  fi

  log "Detected managed project in $PWD; syncing from upgraded knowledge base"
  PROJECT_DIR="$(pwd -P)"
  load_install_settings_from_manifest "$manifest"
  ensure_repo_layout
  run_install
  upgrade_mempalace_graph
}

upgrade_mempalace_graph() {
  local project_wing shared_docs_wing
  # Only run if mempalace was enabled for this project
  if [[ ! "${AGENTIC_ENABLE_MEMPALACE:-}" =~ ^[Yy] ]]; then
    return
  fi

  if ! command -v mempalace >/dev/null 2>&1; then
    return
  fi

  project_wing="$(mempalace_project_wing)"
  shared_docs_wing="$(mempalace_shared_docs_wing)"
  if [[ "$DRY_RUN" == true ]]; then
    log "DRY-RUN mempalace mine \"$PROJECT_DIR\" --wing \"$project_wing\""
    if [[ -d "$PROJECT_DIR/docs" ]]; then
      log "DRY-RUN mempalace mine \"$PROJECT_DIR/docs\" --wing \"$shared_docs_wing\""
    fi
    return
  fi

  log "Refreshing MemPalace knowledge graph for $PROJECT_DIR (wing: $project_wing)"
  if mempalace mine "$PROJECT_DIR" --wing "$project_wing" >/dev/null 2>&1; then
    log "MemPalace graph updated"
  else
    warn "mempalace mine failed; graph may be stale — run manually: mempalace mine \"$PROJECT_DIR\" --wing \"$project_wing\""
  fi

  if [[ -d "$PROJECT_DIR/docs" ]]; then
    log "Refreshing shared MemPalace docs wing from $PROJECT_DIR/docs"
    if mempalace mine "$PROJECT_DIR/docs" --wing "$shared_docs_wing" >/dev/null 2>&1; then
      log "MemPalace shared docs wing updated"
    else
      warn "mempalace docs mine failed; shared docs may be stale — run manually: mempalace mine \"$PROJECT_DIR/docs\" --wing \"$shared_docs_wing\""
    fi
  fi
}

parse_theme_option() {
  local value="${1:-}"
  if [[ -z "$value" ]]; then
    error "Missing --theme value. Allowed: auto|dark|light"
    exit 1
  fi
  if ! validate_theme "$value"; then
    error "Invalid --theme value '$value'. Allowed: auto|dark|light"
    exit 1
  fi
  THEME="$value"
  THEME_EXPLICIT=true
}

handle_no_args() {
  if is_interactive_terminal; then
    ensure_repo_available_for_command
    run_tui
    exit 0
  fi

  usage
  exit 1
}

load_user_config
set_theme_colors
refresh_repo_paths

if [[ $# -eq 0 ]]; then
  handle_no_args
fi

COMMAND="$1"
shift

case "$COMMAND" in
  list)
    ensure_repo_available_for_command
    ensure_repo_layout
    SUBCOMMAND="${1:-}"
    case "$SUBCOMMAND" in
      agentos)
        get_agentos_choices
        ;;
      areas)
        list_areas
        ;;
      specs)
        shift || true
        if [[ "${1:-}" != "--area" ]] || [[ -z "${2:-}" ]]; then
          error "Usage: $SCRIPT_NAME list specs --area <name>"
          exit 1
        fi
        list_specs "$2"
        ;;
      *)
        usage
        exit 1
        ;;
    esac
    ;;

  install)
    while [[ $# -gt 0 ]]; do
      case "$1" in
        --project-dir)
          PROJECT_DIR="$2"
          shift 2
          ;;
        --agent-os)
          if [[ "${#SELECTED_AGENT_OS[@]}" -eq 1 && "${SELECTED_AGENT_OS[0]}" == "$DEFAULT_AGENT_OS" ]]; then
            SELECTED_AGENT_OS=()
          fi
          split_csv "$2" SELECTED_AGENT_OS
          shift 2
          ;;
        --areas)
          split_csv "$2" SELECTED_AREAS
          shift 2
          ;;
        --specializations)
          split_csv "$2" SELECTED_SPECS
          shift 2
          ;;
        --theme)
          parse_theme_option "${2:-}"
          shift 2
          ;;
        --theme=*)
          parse_theme_option "${1#*=}"
          shift
          ;;
        --dry-run)
          DRY_RUN=true
          shift
          ;;
        --no-doctor)
          AGENTIC_DOCTOR=0
          shift
          ;;
        -h|--help)
          usage
          exit 0
          ;;
        *)
          error "Unknown option: $1"
          usage
          exit 1
          ;;
      esac
    done
    if [[ -z "$PROJECT_DIR" && -f "$PWD/$PROJECT_MANIFEST_NAME" ]]; then
      PROJECT_DIR="$PWD"
      load_install_settings_from_manifest "$PWD/$PROJECT_MANIFEST_NAME"
    elif [[ -n "$PROJECT_DIR" && -f "$PROJECT_DIR/$PROJECT_MANIFEST_NAME" ]]; then
      load_install_settings_from_manifest "$PROJECT_DIR/$PROJECT_MANIFEST_NAME"
    fi
    ensure_repo_available_for_command
    ensure_repo_layout
    set_theme_colors
    run_install
    if [[ "$THEME_EXPLICIT" == true ]]; then
      save_user_config
    fi
    ;;

  tui)
    while [[ $# -gt 0 ]]; do
      case "$1" in
        --theme)
          parse_theme_option "${2:-}"
          shift 2
          ;;
        --theme=*)
          parse_theme_option "${1#*=}"
          shift
          ;;
        --dry-run)
          DRY_RUN=true
          shift
          ;;
        --no-doctor)
          AGENTIC_DOCTOR=0
          shift
          ;;
        -h|--help)
          usage
          exit 0
          ;;
        *)
          error "Unknown option: $1"
          usage
          exit 1
          ;;
      esac
    done
    ensure_repo_available_for_command
    run_tui
    ;;

  upgrade)
    while [[ $# -gt 0 ]]; do
      case "$1" in
        --dry-run)
          DRY_RUN=true
          shift
          ;;
        -h|--help)
          usage
          exit 0
          ;;
        *)
          error "Unknown option: $1"
          usage
          exit 1
          ;;
      esac
    done
    upgrade_repo_checkout
    sync_current_project_after_upgrade
    ;;

  self-install)
    while [[ $# -gt 0 ]]; do
      case "$1" in
        --bin-dir)
          SELF_INSTALL_BIN_DIR="$2"
          shift 2
          ;;
        --force)
          SELF_INSTALL_FORCE=true
          shift
          ;;
        --install-fzf)
          SELF_INSTALL_WITH_FZF=true
          shift
          ;;
        --theme)
          parse_theme_option "${2:-}"
          shift 2
          ;;
        --theme=*)
          parse_theme_option "${1#*=}"
          shift
          ;;
        --dry-run)
          DRY_RUN=true
          shift
          ;;
        -h|--help)
          usage
          exit 0
          ;;
        *)
          error "Unknown option: $1"
          usage
          exit 1
          ;;
      esac
    done
    set_theme_colors
    self_install
    if [[ "$THEME_EXPLICIT" == true ]]; then
      save_user_config
    fi
    ;;

  -h|--help)
    usage
    ;;

  -V|--version|version)
    app_version_label
    ;;

  *)
    usage
    exit 1
    ;;
esac
