#!/usr/bin/env bash
# arxael — the friendly front door. No jargon, no JSON, no daemons to think about.
#
# For someone who just started coding: this runs a little always-on helper on your computer that lets a
# bunch of AI coding agents work on your project at the same time without stepping on each other. You only
# ever need a few words:
#
#   arxael up        # turn it on (and connect the project you are standing in)
#   arxael status    # is it working? any problems?
#   arxael logs      # what just happened (in plain words)
#   arxael stop      # turn it off (cleans up after itself)
#
# Everything else — ports, processes, build daemons, work folders — is handled for you and cleaned up for you.
set -u

# ---------------------------------------------------------------- where things live (auto, no setup)
HOME_DIR="${HOME:-/home/ubuntu}"
STATE="${ARXAEL_STATE_DIR:-$HOME_DIR/.arxael}"
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"     # the arxael checkout
PORT_FILE="$STATE/port"; PID_FILE="$STATE/pid"; LOG_FILE="$STATE/daemon.log"
mkdir -p "$STATE"

# colours/symbols (fall back to plain text if not a terminal)
if [ -t 1 ]; then OK=$'\e[32m✓\e[0m'; WARN=$'\e[33m⚠\e[0m'; ERR=$'\e[31m✗\e[0m'; B=$'\e[1m'; D=$'\e[0m'; else OK="OK"; WARN="!"; ERR="x"; B=""; D=""; fi
say() { printf '%s\n' "$*"; }

# ---------------------------------------------------------------- helpers (hidden from the user)
find_gradle_home() {
  for g in "${ARXAEL_GRADLE_HOME:-}" /opt/gradle/gradle-* ; do
    [ -x "$g/bin/gradle" ] && { echo "$g"; return; }
  done
  command -v gradle >/dev/null 2>&1 && dirname "$(dirname "$(readlink -f "$(command -v gradle)")")"
}
# Pick the merge-gate test command from the project in this directory, so `arxael up` "just works" for
# non-gradle stacks instead of defaulting to gradle and erroring on a Python/Rust/Node/Go repo. The daemon
# reads ARXAEL_MERGE_GATE_ADAPTER at startup; a value the user already set always wins.
# True if ANY argument exists. Unmatched globs (nullglob off) arrive as a literal that -e reports false, so
# this is safe to call with a mix of literal names and globs — unlike `ls a b`, which fails if any arg is absent.
_any_exist() { local x; for x in "$@"; do [ -e "$x" ] && return 0; done; return 1; }
detect_gate_adapter() {
  [ -n "${ARXAEL_MERGE_GATE_ADAPTER:-}" ] && { echo "$ARXAEL_MERGE_GATE_ADAPTER"; return; }
  local d="${1:-$PWD}"
  if   _any_exist "$d"/build.gradle* "$d"/settings.gradle*; then echo gradle
  elif _any_exist "$d/pom.xml";                             then echo maven
  elif _any_exist "$d/Cargo.toml";                          then echo cargo
  elif _any_exist "$d/go.mod";                              then echo go
  elif _any_exist "$d"/pyproject.toml "$d"/setup.py "$d"/setup.cfg "$d"/pytest.ini "$d"/tox.ini "$d"/conftest.py "$d"/test_*.py "$d"/*_test.py; then echo pytest
  elif _any_exist "$d/package.json";                        then echo npm
  else echo ""; fi
}

# The daemon launcher. Defaults to the local build, but ARXAEL_CORE_BIN overrides it so a packaged/downloaded
# distribution (e.g. the npm installer's vendored core) can point at its own copy without a source build.
core_bin() { echo "${ARXAEL_CORE_BIN:-$HERE/core/build/install/core/bin/core}"; }
port() { cat "$PORT_FILE" 2>/dev/null || echo 8723; }
is_running() { local p; p="$(cat "$PID_FILE" 2>/dev/null)"; [ -n "$p" ] && kill -0 "$p" 2>/dev/null; }
tok() { cat "$STATE/token" 2>/dev/null; }                                     # local API token (empty if auth off)
api() { curl -fsS -m "${2:-10}" "http://127.0.0.1:$(port)$1"; }              # GET (read endpoints are open)
post() { curl -fsS -m "${3:-120}" -X POST "http://127.0.0.1:$(port)$1" -H 'Content-Type: application/json' -H "X-Arxael-Token: $(tok)" -d "$2"; }
jqv() { command -v jq >/dev/null 2>&1 && jq -r "$1" 2>/dev/null || sed -n 's/.*"'"$2"'":\([0-9]*\).*/\1/p'; }

# ---------------------------------------------------------------- version + update awareness (NOTIFY-ONLY)
# arxael NEVER auto-updates: a pinned/vetted ("authenticated") version must stay put. We only TELL you a newer
# one exists; you update on your terms. Every call here is guarded so it can never block or fail the caller.
_ver_lt() { [ "$1" != "$2" ] && [ "$(printf '%s\n%s\n' "$1" "$2" | sort -V 2>/dev/null | head -1)" = "$1" ]; }
running_version() { api /health 3 2>/dev/null | sed -n 's/.*"version":"\([^"]*\)".*/\1/p' | head -1; }
local_engine_version() {                                   # source VERSION → npm cached engine → running daemon → unknown
  local v="" dist="${ARXAEL_HOME:-$HOME_DIR/.arxael}/dist/current"
  [ -f "$HERE/VERSION" ] && v="$(cat "$HERE/VERSION" 2>/dev/null | head -1 | tr -d '[:space:]')"      # source checkout
  [ -z "$v" ] && [ -f "$dist" ] && v="$(cat "$dist" 2>/dev/null | head -1 | tr -d '[:space:]')"       # npm: cached engine on disk
  [ -z "$v" ] && v="$(running_version)"                                                                # last resort: live daemon
  printf '%s' "${v:-unknown}"
}
# Local-only (no network): warn if the warm daemon runs an OLDER build than what's installed — a restart adopts it.
version_skew_note() {
  is_running || return 0
  local rv lv; rv="$(running_version)"; lv="$(local_engine_version)"
  [ -n "$rv" ] && [ "$lv" != unknown ] && [ "$rv" != "$lv" ] && \
    say "$WARN daemon is running ${B}${rv}${D} but ${B}${lv}${D} is installed — restart to adopt: ${B}arxael stop && arxael up${D}."
  return 0   # never leak the &&-chain's exit status to the caller (e.g. cmd_status / cmd_up)
}
# May we phone GitHub for the latest release? NEVER on the agent/scripted path; honour opt-outs; need curl.
update_check_enabled() {
  [ -t 1 ] || return 1                                      # off when non-interactive — agents never beacon
  [ -n "${ARXAEL_NO_UPDATE_CHECK:-}" ] && return 1
  [ -n "${DO_NOT_TRACK:-}" ] && return 1
  [ -n "${CI:-}" ] && return 1
  command -v curl >/dev/null 2>&1 || return 1
  return 0
}
# Notify-only "update available". Throttled once/day (atomic cache), pinning-aware, never blocks/fails the caller.
update_notice() {
  update_check_enabled || return 0
  local cache="$STATE/update-check" now last latest lv
  now="$(date +%s 2>/dev/null || echo 0)"
  last="$(sed -n 's/^ts=\([0-9]*\).*/\1/p' "$cache" 2>/dev/null)"; case "$last" in *[!0-9]*|"") last=0;; esac
  if [ "$now" -gt 0 ] && [ "$last" -gt 0 ] && [ $(( now - last )) -ge 0 ] && [ $(( now - last )) -lt 86400 ]; then
    latest="$(sed -n 's/^latest=\(.*\)$/\1/p' "$cache" 2>/dev/null)"          # within throttle window: reuse cached
  else
    [ -f "$cache" ] || say "(arxael checks GitHub once/day for updates — turn off with ARXAEL_NO_UPDATE_CHECK=1)"
    latest="$(curl -fsS --connect-timeout 2 -m 2 < /dev/null \
        "https://api.github.com/repos/${ARXAEL_REPO:-alexanderpaquette-byte/arxael}/releases/latest" 2>/dev/null \
        | sed -n 's/.*"tag_name":[ ]*"v\{0,1\}\([^"]*\)".*/\1/p' | head -1)"
    # Only persist a SUCCESSFUL fetch — a transient failure must not poison the throttle (it'd suppress checks
    # for 24h) or leave a tmp file. On failure we simply retry next time.
    if [ -n "$latest" ]; then
      { printf 'ts=%s\nlatest=%s\n' "$now" "$latest" > "$cache.tmp.$$" && mv -f "$cache.tmp.$$" "$cache"; } \
        2>/dev/null || rm -f "$cache.tmp.$$" 2>/dev/null
    fi
  fi
  [ -n "${latest:-}" ] || return 0
  if [ -n "${ARXAEL_ENGINE_VERSION:-}" ]; then                                 # pinned: note it, never nag
    [ "$ARXAEL_ENGINE_VERSION" != "$latest" ] && say "(pinned to ${ARXAEL_ENGINE_VERSION}; latest is ${latest})"
    return 0
  fi
  lv="$(local_engine_version)"
  [ "$lv" != unknown ] && _ver_lt "$lv" "$latest" && \
    say "${B}↑ update available:${D} ${lv} → ${latest}.   Get it: ${B}arxael upgrade${D}   ·   keep this one: ${B}ARXAEL_ENGINE_VERSION=${lv}${D}"
  return 0   # never leak the &&-chain's exit status (would make a clean `arxael up` exit non-zero)
}
cmd_version() {
  say "${B}arxael${D} (engine) $(local_engine_version)"
  is_running && { local rv; rv="$(running_version)"; [ -n "$rv" ] && say "  running daemon: ${rv}"; }
  version_skew_note
  update_notice
}
cmd_upgrade() {
  # NEVER silently auto-updates. On a source checkout, updating is a deliberate pull + rebuild + restart.
  say "${B}Update${D} (source checkout):"
  say "  cd ${HERE} && git pull && ARXAEL_GRADLE_HOME=\$(ls -d /opt/gradle/gradle-* | head -1) ./gradlew :core:installDist"
  say "  then: ${B}arxael stop && arxael up${D}"
  say "Want to keep a specific, vetted version? Pin it: ${B}ARXAEL_ENGINE_VERSION=<x.y.z>${D} (honoured by the npm install)."
}

# ---------------------------------------------------------------- up: turn it on (+ connect this project)
# Decide how much of the machine to use. Sets BUDGET_ENV (extra env for the daemon). Honours flags
# (--max | --budget N | --cores N | --mem NG) for agents/scripts; otherwise asks a human at a terminal;
# otherwise (an agent with no flag) defaults to the whole machine and says how to cap it.
choose_budget() {
  BUDGET_ENV=""
  case "${1:-}" in
    --max)                 BUDGET_ENV="ARXAEL_BUDGET_PCT=100" ;;
    --budget) BUDGET_ENV="ARXAEL_BUDGET_PCT=${2:-100}" ;;
    --cores)  BUDGET_ENV="ARXAEL_CORES=${2:-}" ;;
    --mem)    BUDGET_ENV="ARXAEL_USABLE_RAM_MB=$(( ${2%[gG]} * 1024 ))" ;;
    "" )
      if [ -t 0 ]; then
        say "How much of this machine should it use? (it shares the box with your other apps)"
        say "  ${B}[Enter]${D} all of it (max)   ·   easy mode: a percent like ${B}80${D} / ${B}60${D}"
        say "  exact: ${B}8c${D} (8 cores)   ·   ${B}16g${D} (16 GB)   ·   finest: set ARXAEL_MAX_CONCURRENT / ARXAEL_PER_BUILD_MB (see docs/SETUP.md)"
        printf '> '; read -r ans || ans=""
        case "$ans" in
          "")        BUDGET_ENV="" ;;                                   # max (no cap)
          *c|*C)     BUDGET_ENV="ARXAEL_CORES=${ans%[cC]}" ;;
          *g|*G)     BUDGET_ENV="ARXAEL_USABLE_RAM_MB=$(( ${ans%[gG]} * 1024 ))" ;;
          *[!0-9]*)  say "$WARN Did not understand '\''$ans'\'' — using the whole machine." ;;
          *)         BUDGET_ENV="ARXAEL_BUDGET_PCT=$ans" ;;
        esac
      else
        say "(Using the whole machine. Easy cap: ${B}arxael up --budget 60${D}. Exact: ${B}--cores 8${D} / ${B}--mem 16g${D}. Finest: ARXAEL_MAX_CONCURRENT / ARXAEL_PER_BUILD_MB — see docs/SETUP.md.)"
      fi ;;
    *) say "$WARN Unknown option '\''$1'\'' — using the whole machine." ;;
  esac
}

cmd_up() {
  # Java is REQUIRED — the helper is a JVM daemon. (Gradle is NOT required to run it; see below.)
  if ! command -v java >/dev/null 2>&1 && [ ! -x "${JAVA_HOME:-/nonexistent}/bin/java" ]; then
    say "$ERR The helper is a Java program and I couldn't find Java (21+)."
    say "   One-time setup: ${B}bash $HERE/scripts/install.sh${D} (installs Java + Gradle), then ${B}arxael up${D}."
    exit 1
  fi
  local gh; gh="$(find_gradle_home)"   # OPTIONAL: only needed to BUILD the engine, or to run GRADLE projects
  if [ ! -x "$(core_bin)" ]; then
    # the engine isn't built yet -> building it from source needs Gradle
    if [ -z "$gh" ]; then
      say "$ERR The engine isn't built yet, and building it from source needs Java's build tool (Gradle)."
      say "   One-time: ${B}bash $HERE/scripts/install.sh${D} then ${B}arxael up${D} — or get the prebuilt engine with ${B}npm install -g arxael${D} (no Gradle needed)."
      exit 1
    fi
    say "$WARN First run — building the helper once (takes a minute)…"
    ( cd "$HERE" && ARXAEL_GRADLE_HOME="$gh" ./gradlew -q :core:installDist ) || { say "$ERR Build failed. See above."; exit 1; }
  fi
  # The engine is present. Gradle is OPTIONAL from here: the daemon starts without it, and only GRADLE
  # projects need it — Python/Rust/Node/Go/etc. work gradle-free.
  [ -z "$gh" ] && say "$WARN No Gradle found — the daemon runs and any non-gradle project works; gradle-project builds need Gradle (${B}scripts/install.sh${D} adds it)."

  if is_running; then
    say "$OK Already on."
  else
    local BUDGET_ENV; choose_budget "$@"
    # pick a free port so it never clashes with anything else you are running
    local p=8723; while curl -fsS -m1 "http://127.0.0.1:$p/health" >/dev/null 2>&1; do p=$((p+1)); done
    printf '%s\n' "$p" > "$PORT_FILE.tmp" && mv -f "$PORT_FILE.tmp" "$PORT_FILE"  # atomic: a reader never catches it mid-truncate
    local gate; gate="$(detect_gate_adapter)"
    [ -n "$gate" ] && [ "$gate" != gradle ] && say "  Test gate: ${B}$gate${D} (auto-detected; override with ARXAEL_MERGE_GATE_ADAPTER)."
    say "Starting your helper…"
    env ${BUDGET_ENV} ${gh:+ARXAEL_GRADLE_HOME="$gh"} ${gate:+ARXAEL_MERGE_GATE_ADAPTER="$gate"} ARXAEL_PORT="$p" ARXAEL_STATE_DIR="$STATE" \
      setsid "$(core_bin)" >"$LOG_FILE" 2>&1 < /dev/null &
    echo $! > "$PID_FILE"
    for _ in $(seq 1 50); do api /health 1 >/dev/null 2>&1 && break; sleep 0.2; done
    if ! api /health 2 >/dev/null 2>&1; then say "$ERR It did not start. Last lines of the log:"; tail -5 "$LOG_FILE"; exit 1; fi
    # `arxael up` returning is a hard guarantee the port file is durably readable from another process — agents
    # script `arxael up && cat ~/.arxael/port`, and on some filesystems (notably WSL1) a write isn't flushed
    # cross-process-visible immediately, so the read can come back empty. Flush it before we return.
    sync "$PORT_FILE" 2>/dev/null || sync 2>/dev/null || true
    say "$OK It's on (port $(port))."
  fi

  # If you are standing in a project, connect it so agents can start working on it.
  if git -C . rev-parse --is-inside-work-tree >/dev/null 2>&1; then
    connect_current_project
  else
    say ""
    say "Tip: go into your project folder and run ${B}arxael up${D} again to connect it."
  fi
  say ""
  say "You're set. Check on it any time with ${B}arxael status${D}, or turn it off with ${B}arxael stop${D}."
  update_notice   # AFTER readiness + port-sync, interactive-only — agents (no TTY) skip it, so `up` is never delayed
}

connect_current_project() {
  # The shared copy agents merge into is a bare 'hub' clone kept beside your project — your working files
  # are never touched. Created once, reused after that.
  local top; top="$(git -C . rev-parse --show-toplevel)"
  local name; name="$(basename "$top")"
  local hub="$STATE/hubs/$name.git"
  if [ ! -d "$hub" ]; then
    if ! git -C "$top" rev-parse --verify -q main >/dev/null 2>&1; then
      say "$WARN Your project has no main branch yet, so I cannot connect it. (Commit something on main first.)"
      return
    fi
    say "Connecting ${B}$name${D} (one-time setup of a shared copy)…"
    git clone -q --bare "$top" "$hub" || { say "$ERR Couldn't set up the shared copy."; return; }
    git -C "$hub" config user.email arxael@local; git -C "$hub" config user.name arxael
  else
    # The shared copy already exists. If you committed to your working main outside arxael, the shared copy
    # would be stale and agents would branch off an old base. So fast-forward it to your latest main — but only
    # when that's safe: fetch into a scratch ref (always succeeds), then advance main ONLY if it's a true
    # fast-forward. If the shared copy is already ahead (agents landed work you haven't pulled), leave it; if the
    # two genuinely diverged, don't silently rewrite — warn so you can reconcile. Your working files are untouched.
    if git -C "$top" rev-parse --verify -q main >/dev/null 2>&1; then
      git -C "$hub" fetch -q "$top" "main:refs/arxael/incoming" 2>/dev/null || true
      local inc cur; inc="$(git -C "$hub" rev-parse -q --verify refs/arxael/incoming 2>/dev/null)"; cur="$(git -C "$hub" rev-parse -q --verify main 2>/dev/null)"
      if [ -n "$inc" ] && [ "$inc" != "$cur" ]; then
        if git -C "$hub" merge-base --is-ancestor "$cur" "$inc" 2>/dev/null; then
          git -C "$hub" update-ref refs/heads/main "$inc" && say "$OK Brought your latest ${B}main${D} into the shared copy."
        elif git -C "$hub" merge-base --is-ancestor "$inc" "$cur" 2>/dev/null; then
          : # shared copy is ahead (agents landed work you haven't pulled yet) — expected, nothing to do
        else
          say "$WARN Your ${B}main${D} and the shared copy have diverged; the shared copy was left as-is. Reconcile (e.g. ${B}git pull \"$hub\" main${D}) if needed."
        fi
      fi
      git -C "$hub" update-ref -d refs/arxael/incoming 2>/dev/null || true
    fi
  fi
  # gateWorktrees:0 => let the daemon size the async-gate pool to the box (cores+RAM, H4 autoGateWorktrees);
  # set ARXAEL_GATE_WORKTREES to pin a specific count instead.
  # /merge/register now ACKs immediately (202) and prepares the project in the BACKGROUND — the first
  # build is cold (it probes the module graph + warms the dep cache), which used to block this call past the
  # client timeout and look like a failure. So: a hard failure here (e.g. no commits on main) is still
  # reported at once (422); otherwise we POLL /merge/status registerState and report REAL readiness.
  local res; res="$(post /merge/register "{\"repo\":\"$hub\",\"gateWorktrees\":${ARXAEL_GATE_WORKTREES:-0}}" 30 2>/dev/null)"
  if ! echo "$res" | grep -q '"ok":true'; then
    local err; err="$(echo "$res" | jqv '.error' error)"
    say "$WARN Couldn't connect ${B}$name${D}${err:+ — $err}. Usually no commits on ${B}main${D} yet. See ${B}arxael logs${D}."
    return 0
  fi
  local state="registering" waited=0 said_wait=0 fails=0 m
  while [ "$state" = "registering" ] && [ "$waited" -lt 600 ]; do
    m="$(api /merge/status 5 2>/dev/null)"
    if [ -z "$m" ]; then
      # the status call ITSELF failed (daemon crash / network) — distinct from "still registering"; don't
      # silently spin the full timeout. Tolerate a transient blip, but bail after a few in a row.
      fails=$((fails+1))
      [ "$fails" -ge 3 ] && { say "$WARN Lost contact with the daemon while connecting ${B}$name${D} — try ${B}arxael up${D} again (see ${B}arxael logs${D})."; return 0; }
      sleep 2; waited=$((waited+2)); continue
    fi
    fails=0
    state="$(printf '%s' "$m" | sed -n 's/.*"registerState":"\([a-z]*\)".*/\1/p')"
    # An OLDER daemon (pre-async-register) doesn't expose registerState — it registered SYNCHRONOUSLY, so if it
    # reports registered:true the project is already ready. Fall back to that instead of polling the full timeout
    # for a field it will never send (the version-skew case: new CLI, old daemon).
    [ -z "$state" ] && { printf '%s' "$m" | grep -q '"registered":true' && state="ready" || state="registering"; }
    if [ "$state" = "registering" ]; then
      [ "$said_wait" = 0 ] && { say "$OK Connecting ${B}$name${D} — preparing (first build is cold: probing modules + warming caches)…"; said_wait=1; }
      sleep 2; waited=$((waited+2))
    fi
  done
  case "$state" in
    ready)
      local mods; mods="$(api /merge/status 5 2>/dev/null | jqv '.modules' modules)"
      say "$OK Connected ${B}$name${D}${mods:+ ($mods modules)}. Agents can now submit changes; merged work lands in:"
      say "   $hub   (get it with: ${B}git pull \"$hub\" main${D})"
      ;;
    failed)
      local err; err="$(api /merge/status 5 2>/dev/null | jqv '.registerError' registerError)"
      say "$WARN Couldn't connect ${B}$name${D}${err:+ — $err}. See ${B}arxael logs${D}."
      ;;
    *)
      say "$WARN ${B}$name${D} is still preparing after ${waited}s — it should be ready shortly; check ${B}arxael status${D}."
      ;;
  esac
}

# ---------------------------------------------------------------- status: plain-language health
cmd_status() {
  if ! is_running; then say "$ERR Not running. Turn it on with ${B}arxael up${D}."; exit 1; fi
  local h; h="$(api /health 5 2>/dev/null)" || { say "$ERR Running but not answering — try ${B}arxael stop${D} then ${B}arxael up${D}."; exit 1; }
  local cores tgt errs ram bind
  cores="$(echo "$h" | jqv '.cores' cores)"; tgt="$(echo "$h" | jqv '.concurrencyTarget' concurrencyTarget)"
  errs="$(echo "$h" | jqv '.recentErrorCount' recentErrorCount)"
  ram="$(echo "$h" | jqv '.usableRamMb' usableRamMb)"; bind="$(echo "$h" | sed -n 's/.*"bindingConstraint":"\([a-z]*\)".*/\1/p')"
  local ramg=""; [ -n "${ram:-}" ] && ramg="$(( ram / 1024 )) GB RAM"
  say "$OK ${B}On${D} — detected ${cores:-?} cores${ramg:+ + $ramg} → running up to ${B}${tgt:-?}${D} builds at once${bind:+ (limited by: $bind)}."
  say "   (auto-sized to this machine; it adjusts itself as load + memory change — nothing to configure.)"
  version_skew_note
  local m; m="$(api /merge/status 5 2>/dev/null)"
  if echo "$m" | grep -q '"registered":true'; then
    local landed reverts inflight queued msg
    landed="$(echo "$m" | jqv '.landed' landed)"; reverts="$(echo "$m" | jqv '.reverts' reverts)"; inflight="$(echo "$m" | jqv '.inFlightGates' inFlightGates)"
    queued="$(echo "$m" | jqv '.queueDepth' queueDepth)"
    msg="$OK ${B}${landed:-0}${D} change(s) merged so far"
    [ "${reverts:-0}" != 0 ] && msg="$msg, ${reverts} undone (did not pass tests)"
    [ "${queued:-0}" != 0 ] && msg="$msg, ${queued} waiting in line"
    [ "${inflight:-0}" != 0 ] && msg="$msg, ${inflight} being checked right now"
    say "$msg."
  else
    say "→ Next: go into your project folder and run ${B}arxael up${D} to connect it."
  fi
  if [ "${errs:-0}" != "0" ] && [ -n "${errs:-}" ]; then
    say "$WARN ${errs} recent problem(s). See them with ${B}arxael logs${D}."
  else
    say "$OK No problems."
  fi
}

# ---------------------------------------------------------------- logs: recent activity, in plain words
cmd_logs() {
  local f="$STATE/events.jsonl"
  [ -f "$f" ] || { say "Nothing yet."; exit 0; }
  say "${B}Recent activity:${D}"
  tail -n 40 "$f" | while IFS= read -r line; do
    case "$line" in
      *merge_land*)        say "  $OK merged a change" ;;
      *merge_revert*)      say "  $WARN undid a change that broke tests" ;;
      *merge_bounce*)      say "  $WARN sent a change back (did not pass / conflicted)" ;;
      *_error*|*_failed*)  say "  $ERR problem: $(echo "$line" | sed -n 's/.*"error":"\([^"]*\)".*/\1/p')" ;;
      *invoke_overloaded*) say "  $WARN busy — a build had to wait" ;;
      *governor_resize*)   say "  $OK adjusted how many builds run at once (matching your machine)" ;;
      *merge_register*)    say "  $OK connected a project" ;;
    esac
  done
}

# ---------------------------------------------------------------- verify: prove the system actually works
# One command for "does it work?" — builds, runs the unit tests, the acceptance smoke (a real daemon + real
# build), and the multi-language end-to-end proof (real toolchains, self-skips any that aren't installed).
# This is how we DECIDE things are good here: proof, not assertion.
cmd_verify() {
  local GH; GH="$(find_gradle_home)"
  [ -n "$GH" ] || { say "$ERR No Gradle/JDK found. Run ${B}scripts/install.sh${D} first."; exit 1; }
  if is_running; then say "$WARN The daemon is running; ${B}arxael stop${D} first (verify starts its own)."; exit 1; fi
  local log; log="$(mktemp)"
  export ARXAEL_GRADLE_HOME="$GH"

  say "→ unit tests + coverage"
  if "$HERE/gradlew" -p "$HERE" -q :core:test :core:installDist >"$log" 2>&1; then say "  $OK unit tests pass"
  else say "  $ERR unit tests failed:"; tail -25 "$log"; rm -f "$log"; exit 1; fi

  say "→ acceptance smoke (real daemon, real build, allowlist, /metrics, adapters)"
  if bash "$HERE/scripts/smoke.sh" >"$log" 2>&1; then say "  $OK smoke green"
  else say "  $ERR smoke failed:"; tail -25 "$log"; rm -f "$log"; exit 1; fi

  say "→ multi-language end-to-end (real toolchains; skips any not installed)"
  if bash "$HERE/bench/lang_smoke.sh" >"$log" 2>&1; then
    say "  $OK $(grep -oE '[0-9]+ real-toolchain invocations' "$log" | head -1 || echo 'multi-language') proven"
  else say "  $ERR multi-language proof failed:"; tail -25 "$log"; rm -f "$log"; exit 1; fi

  rm -f "$log"
  say "$OK Verified — builds, tests green, smoke green, multi-language works."
  say "  (throughput proof — the per-worktree-home win — is ${B}bench/merge_http_load.py${D}; merge-on-pytest is ${B}bench/lang_merge_smoke.py${D}.)"
}

# ---------------------------------------------------------------- bench: prove it lands more PRs/min
# The decision criterion for "is a change good?" is throughput. This runs the real MergeOrchestrator over
# HTTP against the wide-DAG fixture and prints merges/min — and with --ab, runs it BOTH with and without the
# per-worktree-home cache-lock fix so you can SEE the win, not take it on faith.
cmd_bench() {
  local GH; GH="$(find_gradle_home)"
  [ -n "$GH" ] || { say "$ERR No Gradle/JDK found. Run ${B}scripts/install.sh${D} first."; exit 1; }
  if is_running; then say "$WARN The daemon is running; ${B}arxael stop${D} first (bench starts its own)."; exit 1; fi
  export ARXAEL_GRADLE_HOME="$GH"
  "$HERE/gradlew" -p "$HERE" -q :core:installDist >/dev/null 2>&1 || { say "$ERR build failed"; exit 1; }
  # The benchmark needs the wide-DAG gradle fixture. Generate it once if missing — a fresh box (or one whose
  # /tmp was cleared, e.g. after a resize) has none, and without this the driver fails deep inside with an
  # opaque FileNotFoundError that the per-arm error capture below would otherwise be the only hint of.
  local FIX=/tmp/wide-dag
  if [ ! -d "$FIX" ]; then
    say "→ generating the wide-DAG benchmark fixture (one-time)…"
    python3 "$HERE/bench/fixtures/gen_fixture.py" "$FIX" --modules 8 --classes 30 --methods 3 --fanin 3 >/dev/null 2>&1 \
      || { say "$ERR couldn't generate the benchmark fixture ($FIX)"; exit 1; }
  fi
  local agents="${2:-6}" cores="${3:-12}" window="${4:-60}"
  run_one() { # $1 = per-worktree-home true|false ; echoes "N merges/min (p50 ...)"
    local out="/tmp/arxael-bench-$1.jsonl" log="/tmp/arxael-bench-$1.log"; rm -f "$out"
    ARXAEL_PER_WORKTREE_HOME="$1" python3 "$HERE/bench/merge_http_load.py" --auto-warm --fixture "$FIX" \
      --agents "$agents" --cores "$cores" --max-concurrent "$cores" --reserved-high 2 \
      --window "$window" --threshold 4 --port 8795 --root "/tmp/arxael-bench-$1" --out "$out" >"$log" 2>&1
    python3 -c "import json;d=json.loads(open('$out').read().splitlines()[-1]);print('%(merges_per_min)s merges/min (p50 %(p50TimeToLandMs)sms, landed=%(landed)s, reverts=%(reverts)s)'%d)" 2>/dev/null \
      || { echo "(no result — last lines of $log:)"; tail -3 "$log" 2>/dev/null | sed 's/^/    /'; }
  }
  if [ "${1:-}" = "--ab" ]; then
    say "→ A/B: per-worktree-home ON vs OFF (${agents} agents, ${cores} cores, ${window}s each)…"
    local on off
    on="$(run_one true | tail -1)"; off="$(run_one false | tail -1)"
    say "  ON  (lock removed): $on"
    say "  OFF (shared lock):  $off"
  else
    say "→ throughput (${agents} agents, ${cores} cores, ${window}s)…"
    say "  $(run_one true | tail -1)"
  fi
}

# ---------------------------------------------------------------- pull: bring merged work back to your checkout
# Landed changes live in the shared 'hub' copy; this fast-forwards your working checkout to them (the return
# trip — `arxael up` already syncs YOUR commits INTO the hub). Your files are only ever moved forward.
cmd_pull() {
  if ! git -C . rev-parse --is-inside-work-tree >/dev/null 2>&1; then say "$ERR Go into your project folder first."; exit 1; fi
  local top name hub; top="$(git -C . rev-parse --show-toplevel)"; name="$(basename "$top")"; hub="$STATE/hubs/$name.git"
  [ -d "$hub" ] || { say "$WARN ${B}$name${D} isn't connected yet — run ${B}arxael up${D} here first."; exit 1; }
  if git -C "$top" pull --ff-only "$hub" main >/dev/null 2>&1; then
    say "$OK Pulled the latest merged work into ${B}$name${D}."
  else
    say "$WARN Couldn't fast-forward (your checkout has local commits the shared copy doesn't). Reconcile: ${B}git pull \"$hub\" main${D}."
  fi
}

# ---------------------------------------------------------------- stop: turn off, clean up after itself
cmd_stop() {
  if is_running; then post /shutdown '{}' 2 >/dev/null 2>&1 || true; sleep 2; fi
  bash "$HERE/scripts/reap-daemons.sh" >/dev/null 2>&1 || true   # belt-and-braces: clear any leftover build daemons
  rm -f "$PID_FILE"
  say "$OK Off. Everything cleaned up."
}

case "${1:-}" in
  up|start)   shift; cmd_up "$@" ;;
  status)     cmd_status ;;
  logs)       cmd_logs ;;
  stop|down)  cmd_stop ;;
  pull)       cmd_pull ;;
  verify|prove) cmd_verify ;;
  version|--version|-v) cmd_version ;;
  upgrade|update) cmd_upgrade ;;
  bench)      cmd_bench "${2:-}" "${3:-}" "${4:-}" "${5:-}" ;;
  ""|help|-h|--help)
    say "${B}arxael${D} — many AI agents, one project, no mess."
    say ""
    say "${B}You only need three words:${D}"
    say "  ${B}arxael up${D}      turn it on (and connect the project you're in)"
    say "  ${B}arxael status${D}  is it working? any problems?"
    say "  ${B}arxael stop${D}    turn it off (cleans up after itself)"
    say ""
    say "AI agents don't run any commands — after ${B}arxael up${D} they just call the API:"
    say "  curl -s 127.0.0.1:\$(cat ~/.arxael/port 2>/dev/null || echo 8723)/   ${D}# self-describing; see AGENTS.md${D}"
    say ""
    say "Occasionally useful: ${B}pull${D} (get merged work into your checkout) · ${B}logs${D} (what happened) · ${B}verify${D} (prove it works) · ${B}bench${D} (throughput)"
    say "  ${B}version${D} (what's running + skew) · ${B}upgrade${D} (how to update — never automatic; pin a vetted build with ARXAEL_ENGINE_VERSION)" ;;
  *) say "$ERR Unknown command: $1. Try ${B}arxael help${D}." ; exit 1 ;;
esac
