#!/bin/bash
# AI-OS Core bootstrap v2 — state contract schemaVersion 2
# Contract: COMPLETE/BLOCKED/RUNNING; sole writer of ~/.aios-core/* state.
set -euo pipefail
set -E  # errtrace: ERR trap must fire inside functions/subshell-free calls, or B2 protection silently vanishes
WORKER_VERSION="2.1.0"
# User-facing installer wizard version — single source of truth: public/aios-core/VERSION
# (kept in sync by scripts/build-fleet-checksums.sh). Shown in the launch banner, the web
# download page, and the DMG installer app so a user can confirm they're on the latest.
WIZARD_VERSION="2.5.3"
PROFILE="ai-os-core"
INSTALL_ROOT="${AIOS_INSTALL_ROOT:-$HOME/ai-os}"
HOSTED_BASE_URL="${AIOS_HOSTED_BASE_URL:-https://install.hatchedstacks.com}"
INSTALL_TOOLS=0
HERMES_STACK=0
UPDATE_STACK=0
COMPOSIO=0
COMPANIONS=0
COMPANIONS_ONLY=all
OMEGA=0
TERMINAL=0
CONNECT_STACK=0
RESTORE_KEYS=0
KEYS_STATUS=0
BACKUP_GUARD=0
REPORT_ONLY=0
VERIFY_STACK=0
RESTORE_MODE="${AIOS_RESTORE_MODE:-core-stack}"
SCAN_ONLY=0
DISK_CHECK=0
PLAN_ONLY=0
JSON_STATE=0
DASHBOARD_REPO="${AIOS_DASHBOARD_REPO:-https://github.com/jrdaws/ai-os-dashboard.git}"
LOCAL_DASHBOARD_URL="${AIOS_LOCAL_DASHBOARD_URL:-http://127.0.0.1:9119/onboarding?path=core}"
PACKET_NAME="${AIOS_CORE_PACKET_NAME:-ai-os-core-packet-v1.tar.gz}"
PACKET_DIR_NAME="${AIOS_CORE_PACKET_DIR_NAME:-ai-os-core-packet-v1}"
STATE_DIR="$HOME/.aios-core"
MANIFEST_DEST="$STATE_DIR/manifest.json"
STATE_FILE="$STATE_DIR/state.json"
NEXT_STEP_FILE="$STATE_DIR/NEXT_STEP.txt"
SUMMARY_FILE="$STATE_DIR/INSTALL_SUMMARY.txt"
COMPLETE_FILE="$STATE_DIR/RESTORE_COMPLETE"
CANONICAL_SELF="$STATE_DIR/download/bootstrap.sh"
RERUN_CMD="bash $CANONICAL_SELF --profile ai-os-core --install-tools"
STARTED_AT=""
CURRENT_PHASE="preflight"

usage() {
  cat <<'USAGE'
AI-OS Core bootstrap v2

Usage:
  bash bootstrap.sh [--profile ai-os-core] [--install-root ~/ai-os] [--install-tools] [--scan] [--json-state] [--hermes-stack] [--update] [--composio] [--report] [--verify-stack] [--provision] [--companions [name]] [--connect-stack] [--sync-private] [--restore-keys] [--keys-status] [--backup-guard] [--restore-mode core-stack|owner-restore|contractor-va-workstation|client-operator-machine] [--yes]

Modes:
  --install-tools   full install/repair
  --scan            detection only: refresh gates[] in state.json, exit 0, mutate nothing else
  --disk-check      report free space against the WARN/PAUSE/velocity floors and
                    exit 0, or refuse with exit 4 under PAUSE. Writes nothing.
  --plan            dry run: report what a real install WOULD do (gates, phases,
                    predicted outcome) and exit 0. Writes nothing at all.
  --json-state      with --scan: also print state.json to stdout
  --hermes-stack    W6.5: run bundled install-hermes-stack.sh (receipts in ~/.aios-core)
  --update          with --hermes-stack: update an EXISTING install in place (no
                    uninstall). Applies generic append-only improvements only;
                    personalized config/memory/keys are preserved.
  --omega           install generic OMEGA persistent-memory for Claude Code
                    (venv + omega-memory pkg + append-only MCP/hooks; fresh
                    per-machine store, no copied data). Opt-in.
  --terminal        recreate the AI-OS terminal env for the current user (Ghostty +
                    font + generic tmux/ghostty configs + helper scripts).
  --composio        W6.5: run bundled install-composio-connect.sh (key via Keychain gate)
  --report          generate redacted INSTALL_REPORT.txt/.json for support (no install action)
  --restore-mode M  select restore scope: core-stack, owner-restore, contractor-va-workstation, client-operator-machine

Terminal outcome contract:
  COMPLETE: prints AI-OS CORE RESTORE COMPLETE, writes ~/.aios-core/state.json and proof files.
  BLOCKED:  prints AI-OS CORE RESTORE BLOCKED: <id>, writes state.json and NEXT_STEP.txt with exactly one next command.
USAGE
}

# D3 hardening: this stack is Apple Silicon only (DMG aarch64, brew paths /opt/homebrew)
if [ "$(uname -m)" != "arm64" ]; then
  echo "AI-OS CORE RESTORE BLOCKED: unsupported-architecture" >&2
  echo "This installer requires an Apple Silicon Mac (M1 or newer). This Mac is $(uname -m)." >&2
  exit 4
fi

while [[ $# -gt 0 ]]; do
  case "$1" in
    --profile) PROFILE="${2:-}"; shift 2 ;;
    --install-root) INSTALL_ROOT="${2:-}"; shift 2 ;;
    --install-tools) INSTALL_TOOLS=1; shift ;;
    --scan) SCAN_ONLY=1; shift ;;
    --disk-check) DISK_CHECK=1; shift ;;
    --plan) PLAN_ONLY=1; shift ;;
    --json-state) JSON_STATE=1; shift ;;
    --hermes-stack) HERMES_STACK=1; shift ;;
    --update) UPDATE_STACK=1; shift ;;
    --composio) COMPOSIO=1; shift ;;
    --companions) COMPANIONS=1; shift; case "${1:-}" in aegis|disk-guard|va-access|ollama|openbrain|codegraph|litellm|terminal|skills|postiz|fleet-node|remote-access|all) COMPANIONS_ONLY="$1"; shift ;; esac ;;
    --omega) OMEGA=1; shift ;;
    --terminal) TERMINAL=1; shift ;;
    --sync-private) SYNC_PRIVATE=1; shift ;;
    --provision) PROVISION=1; shift ;;
    --connect-stack) CONNECT_STACK=1; shift ;;
    --restore-keys) RESTORE_KEYS=1; shift ;;
    --keys-status) KEYS_STATUS=1; shift ;;
    --backup-guard) BACKUP_GUARD=1; shift ;;
    --report) REPORT_ONLY=1; shift ;;
    --verify-stack) VERIFY_STACK=1; shift ;;
    --restore-mode) RESTORE_MODE="${2:-}"; shift 2 ;;
    --yes|-y) shift ;;
    --help|-h) usage; exit 0 ;;
    *) echo "Unknown argument: $1" >&2; usage >&2; exit 2 ;;
  esac
done

# The worker performs recursive creation and a guarded dashboard replacement,
# so it accepts exactly one installation target. Reject alternate roots and
# symlink redirection before any state or install directory is mutated.
if [[ "$INSTALL_ROOT" == '~/'* ]]; then INSTALL_ROOT="$HOME/${INSTALL_ROOT#\~/}"; fi
CANONICAL_INSTALL_ROOT="$HOME/ai-os"
if [[ "$INSTALL_ROOT" != "$CANONICAL_INSTALL_ROOT" ]]; then
  echo "AI-OS CORE RESTORE BLOCKED: invalid-install-root" >&2
  echo "The only supported install root is: $CANONICAL_INSTALL_ROOT" >&2
  exit 4
fi
if [[ -L "$CANONICAL_INSTALL_ROOT" ]]; then
  echo "AI-OS CORE RESTORE BLOCKED: install-root-symlink" >&2
  echo "Refusing an install root that is a symbolic link: $CANONICAL_INSTALL_ROOT" >&2
  exit 4
fi

# Every mode writes or may later write beneath ~/.aios-core. Reject redirection
# before scan/report/W6.5/full-install dispatch, including dangling symlinks.
if [[ -L "$STATE_DIR" ]]; then
  echo "AI-OS CORE RESTORE BLOCKED: state-dir-symlink" >&2
  echo "Refusing a state directory that is a symbolic link: $STATE_DIR" >&2
  exit 4
fi
if [[ -e "$STATE_DIR" && ! -d "$STATE_DIR" ]]; then
  echo "AI-OS CORE RESTORE BLOCKED: state-dir-not-directory" >&2
  echo "Refusing a state path that is not a directory: $STATE_DIR" >&2
  exit 4
fi
if [[ -L "$STATE_DIR/download" ]]; then
  echo "AI-OS CORE RESTORE BLOCKED: download-dir-symlink" >&2
  echo "Refusing a durable download directory that is a symbolic link: $STATE_DIR/download" >&2
  exit 4
fi
if [[ -e "$STATE_DIR/download" && ! -d "$STATE_DIR/download" ]]; then
  echo "AI-OS CORE RESTORE BLOCKED: download-dir-not-directory" >&2
  echo "Refusing a durable download path that is not a directory: $STATE_DIR/download" >&2
  exit 4
fi

# ── consolidated audit report (support/feedback channel) ──
generate_report() {
  # best-effort, never fatal. Script lives beside this file in the .app bundle.
  local rdir
  rdir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/scripts"
  if [[ -f "$rdir/generate-install-report.sh" ]]; then
    bash "$rdir/generate-install-report.sh" >/dev/null 2>&1 || true
    printf 'Audit report (redacted, safe to share): %s\n' "$STATE_DIR/INSTALL_REPORT.txt"
  fi
}

case "$PROFILE" in
  ai-os-core) ;;
  future-custom) echo "Profile future-custom is reserved; continuing with ai-os-core behavior." >&2 ;;
  *) echo "Unsupported profile: $PROFILE" >&2; echo "Supported profiles: ai-os-core, future-custom" >&2; exit 2 ;;
esac

case "$RESTORE_MODE" in
  core-stack|owner-restore|contractor-va-workstation|client-operator-machine) ;;
  *) echo "Unsupported restore mode: $RESTORE_MODE" >&2; echo "Supported restore modes: core-stack, owner-restore, contractor-va-workstation, client-operator-machine" >&2; exit 2 ;;
esac

if [[ "${_AIOS_INTERNAL_TEST_OUTCOME:-${AIOS_TEST_OUTCOME:-}}" == "" && "$(uname -s)" != "Darwin" ]]; then
  echo "AI-OS Core bootstrap currently targets macOS. Detected: $(uname -s)" >&2
  exit 1
fi

log() { printf '\n==> %s\n' "$*"; }
warn() { printf '\nWARN: %s\n' "$*" >&2; }
have() { command -v "$1" >/dev/null 2>&1; }

# ── Disk pressure gate (AGENTS.md rule 10) ──────────────────────────────────
# free >= 10 GiB OK; under 10 GiB WARN; under 5 GiB PAUSE (refuse). Plus a
# velocity rule: escalate to WARN when projected time-to-empty is under 6 hours
# regardless of absolute free space. On 2026-08-03 this Mac fell from 104.8 GiB
# to 0.39 GiB in twenty hours; at that burn rate the 10 GiB floor alone gives
# about 1.9h of warning, where the projection gives about 6h.
#
# Thresholds are POLICY and live here as values, not as magic numbers buried in
# a comparison. They are overridable so the guard test can synthesise pressure
# without a measurement backdoor: raising the PAUSE floor is a real, supported
# configuration, so the test drives the same code path a user would.
DISK_WARN_GIB="${AIOS_DISK_WARN_GIB:-10}"
DISK_PAUSE_GIB="${AIOS_DISK_PAUSE_GIB:-5}"
DISK_VELOCITY_HOURS="${AIOS_DISK_VELOCITY_HOURS:-6}"
# Sample ledger, declared per rule 10:
#   target:      $STATE_DIR/disk-samples
#   generations: 20 lines, rewritten atomically (temp -> verify -> mv) each run
#   max bytes:   under 512 B, hard-bounded by the keep count, never grows
#   verify:      awk '/^[0-9]+ [0-9]+$/' ~/.aios-core/disk-samples | wc -l
DISK_SAMPLES="$STATE_DIR/disk-samples"
DISK_SAMPLES_KEEP=20

disk_free_gib() {
  local v
  # `|| v=""` is load-bearing. Under `set -e` + `pipefail` + the ERR trap, a
  # failing df aborts the whole run BEFORE the fallback below can run, which
  # made the fallback dead code in exactly the case it exists for. Marking the
  # assignment as handled keeps the failure local and fails CLOSED to 0 GiB,
  # which reads as maximum pressure and refuses.
  v="$(df -g "$HOME" 2>/dev/null | awk 'NR==2 {print $4}')" || v=""
  case "${v:-}" in ''|*[!0-9]*) v=0 ;; esac
  printf '%s' "$v"
}

# Projected whole hours until the volume is empty, from the oldest sample inside
# a 24h window. Prints nothing when unknown: fewer than two samples, a span too
# short to be meaningful, or a non-positive burn rate (space was freed). A fresh
# Mac has no history, so absolute thresholds govern the first run. That is
# honest -- a velocity needs two measurements and cannot be invented from one.
disk_velocity_hours() {
  [ -f "$DISK_SAMPLES" ] || return 0
  local now free
  now="$(date -u +%s)"
  free="$(disk_free_gib)"
  awk -v now="$now" -v free="$free" -v minspan=300 -v maxage=86400 '
    /^[0-9]+ [0-9]+$/ {
      if (t == "" && (now - $1) <= maxage && (now - $1) >= minspan) { t = $1; f = $2 }
    }
    END {
      if (t == "") exit 0
      span = now - t
      drop = f - free
      if (drop <= 0) exit 0
      rate = drop / span
      if (rate <= 0) exit 0
      printf "%d", int(free / rate / 3600)
    }' "$DISK_SAMPLES" 2>/dev/null || true
}

# Append one sample. Never creates STATE_DIR: the gate runs before the install
# root exists and must not mutate anything on its way to refusing.
disk_record_sample() {
  [ -d "$STATE_DIR" ] || return 0
  local tmp="$DISK_SAMPLES.tmp.$$"
  {
    [ -f "$DISK_SAMPLES" ] && tail -n "$((DISK_SAMPLES_KEEP - 1))" "$DISK_SAMPLES"
    printf '%s %s\n' "$(date -u +%s)" "$(disk_free_gib)"
  } > "$tmp" 2>/dev/null && mv -f "$tmp" "$DISK_SAMPLES" || rm -f "$tmp"
  return 0
}

# The command a refusal tells the user to run. The durable copy is created by
# self_copy, which has NOT run on a first install, so advertising it
# unconditionally hands the user a path that does not exist yet. Fall back to
# the invocation that actually got us here.
disk_rerun_cmd() {
  if [ -f "$CANONICAL_SELF" ]; then
    printf '%s' "$RERUN_CMD"
  else
    printf 'bash %s --profile ai-os-core --install-tools' "${BASH_SOURCE[0]}"
  fi
}

# The band this run measured, so callers report what the gate actually decided
# instead of recomputing policy at the print site.
DISK_BAND="unknown"

# The gate. $1 = GiB this phase needs, $2 = human label.
#
# Refusal has TWO idioms and picking the wrong one is a shipped-installer bug:
#   * before state.json exists -- bare BLOCKED line, exit 4, write NOTHING. This
#     is the idiom the other preflight guards above use.
#   * after write_state_running has already written state.json -- the run OWES a
#     terminal outcome. Exiting bare leaves status=running with a dead pid, and
#     the GUI reads that as "Install interrupted" with a [Resume install] button
#     that hits the same gate and returns to the same screen forever, with the
#     word "disk" never reaching any durable artifact.
# It is called again before each bulk write, because free space can collapse
# DURING a long brew run -- the per-phase needs re-measure, they do not invent a
# threshold stricter than the policy floor.
# Evaluation, separated from action. Sets DISK_BAND / DISK_FREE / DISK_REQUIRED
# and returns 0 if the phase may proceed, 1 if policy says refuse. It NEVER
# exits and NEVER writes, so a dry run can ask "what would happen" without
# triggering the consequence. disk_gate is the executor over this decision.
disk_evaluate() {
  local need="$1" vhours
  DISK_FREE="$(disk_free_gib)"
  DISK_REQUIRED="$DISK_PAUSE_GIB"
  [ "$need" -gt "$DISK_REQUIRED" ] && DISK_REQUIRED="$need"
  if [ "$DISK_FREE" -lt "$DISK_REQUIRED" ]; then
    DISK_BAND="pause"
    return 1
  fi
  vhours="$(disk_velocity_hours)"
  DISK_VELOCITY="$vhours"
  if [ "$DISK_FREE" -lt "$DISK_WARN_GIB" ]; then
    DISK_BAND="warn-low"
  elif [ -n "$vhours" ] && [ "$vhours" -lt "$DISK_VELOCITY_HOURS" ]; then
    DISK_BAND="warn-velocity"
  else
    DISK_BAND="ok"
  fi
  return 0
}
DISK_FREE=0
DISK_REQUIRED=0
DISK_VELOCITY=""

disk_gate() {
  local need="$1" label="$2" free required vhours rerun
  free="$(disk_free_gib)"
  required="$DISK_PAUSE_GIB"
  [ "$need" -gt "$required" ] && required="$need"
  if [ "$free" -lt "$required" ]; then
    DISK_BAND="pause"
    rerun="$(disk_rerun_cmd)"
    # --disk-check must stay pure even when a state file already exists.
    if [ "${DISK_CHECK:-0}" != "1" ] && [ -f "$STATE_FILE" ]; then
      block_and_exit "disk-pressure" \
        "Not enough free disk space for ${label}: ${free} GiB free, ${required} GiB required. Empty the Trash or remove large downloads, then run the command below." \
        "$rerun" "Free up disk space and rerun"
    fi
    echo "AI-OS CORE RESTORE BLOCKED: disk-pressure" >&2
    echo "Not enough free disk space for ${label}: ${free} GiB free, ${required} GiB required." >&2
    echo "Free up space (empty the Trash, remove large downloads), then run:" >&2
    echo "[" >&2
    echo "$rerun" >&2
    echo "]" >&2
    exit 4
  fi
  vhours="$(disk_velocity_hours)"
  if [ "$free" -lt "$DISK_WARN_GIB" ]; then
    DISK_BAND="warn-low"
    warn "Low disk space: ${free} GiB free, under the ${DISK_WARN_GIB} GiB comfort floor. Continuing ${label}."
  elif [ -n "$vhours" ] && [ "$vhours" -lt "$DISK_VELOCITY_HOURS" ]; then
    DISK_BAND="warn-velocity"
    warn "Disk is filling fast: projected time-to-empty about ${vhours}h, under the ${DISK_VELOCITY_HOURS}h velocity floor. Continuing ${label}."
  else
    DISK_BAND="ok"
  fi
  return 0
}

orbstack_docker_ready() {
  have docker \
    && DOCKER_CONTEXT=orbstack docker context inspect orbstack >/dev/null 2>&1 \
    && DOCKER_CONTEXT=orbstack docker info --format '{{.Name}}' 2>/dev/null | grep -qx 'orbstack' \
    && DOCKER_CONTEXT=orbstack docker version >/dev/null 2>&1
}

# ── Pure-shell JSON escaping (no python3 — CLT stub on fresh Macs) ──
json_escape() {
  local s=$1
  s=${s//\\/\\\\}; s=${s//\"/\\\"}
  s=${s//$'\n'/\\n}; s=${s//$'\r'/\\r}; s=${s//$'\t'/\\t}
  printf '%s' "$s"
}
jstr() { printf '"%s"' "$(json_escape "$1")"; }

boot_epoch() {
  # kern.boottime returns "{ sec = N, usec = M } <date>" — extract N only.
  sysctl -n kern.boottime 2>/dev/null | sed 's/.*sec = \([0-9]*\),.*/\1/' | grep -E '^[0-9]+$' || echo 0
}

now_utc() { date -u +%Y-%m-%dT%H:%M:%SZ; }

# ── Durable download generation ──
# bootstrap.sh, scripts/, skills/, and fleet-checksums.sha256 are one generation.
# Build that generation beside download/, then replace the whole directory. If
# the second rename fails, put the prior generation back before returning.
validate_payload_tree() {
  local root="$1" label="$2" unsafe
  if [[ -L "$root" || ! -d "$root" ]]; then
    echo "[bootstrap] BLOCKED: $label payload is not a regular directory" >&2
    return 1
  fi
  if ! unsafe="$(find "$root" ! -type d ! -type f -print -quit)"; then
    echo "[bootstrap] BLOCKED: $label payload could not be inspected safely" >&2
    return 1
  fi
  if [[ -n "$unsafe" ]]; then
    echo "[bootstrap] BLOCKED: $label payload contains a link or special entry" >&2
    return 1
  fi
}

promote_download_stage() {
  local stage="$1" destination="$STATE_DIR/download" backup="$STATE_DIR/.download-old.$$" had_old=0
  case "$stage" in
    "$STATE_DIR"/.download-stage.*) ;;
    *) echo "[bootstrap] BLOCKED: unsafe durable-download stage path" >&2; return 1 ;;
  esac
  [[ -d "$stage" && ! -L "$stage" ]] || {
    echo "[bootstrap] BLOCKED: durable-download stage is not a regular directory" >&2
    return 1
  }
  if [[ -L "$destination" || ( -e "$destination" && ! -d "$destination" ) ]]; then
    echo "[bootstrap] BLOCKED: durable download path is not a regular directory" >&2
    return 1
  fi

  rm -rf "$backup"
  if [[ -d "$destination" ]]; then
    mv "$destination" "$backup" || return 1
    had_old=1
  fi
  if ! mv "$stage" "$destination"; then
    rm -rf "$destination"
    if [[ "$had_old" == "1" ]]; then
      if ! mv "$backup" "$destination"; then
        echo "[bootstrap] CRITICAL: durable download promotion and rollback both failed" >&2
        return 1
      fi
      echo "[bootstrap] BLOCKED: durable download promotion failed; prior generation restored" >&2
    else
      echo "[bootstrap] BLOCKED: durable download promotion failed before first installation" >&2
    fi
    return 1
  fi
  rm -rf "$backup"
}

# ── Self-copy: canonical rerun location survives both launch modes ──
self_copy() {
  local src="${BASH_SOURCE[0]}" payload_dir stage
  if [[ ! -f "$src" || "$src" == "$CANONICAL_SELF" ]]; then return 0; fi

  payload_dir="$(cd "$(dirname "$src")" 2>/dev/null && pwd -P)" || return 1
  if [[ -e "$payload_dir/scripts" || -L "$payload_dir/scripts" ]]; then
    validate_payload_tree "$payload_dir/scripts" "scripts" || return 1
  fi
  if [[ -e "$payload_dir/skills" || -L "$payload_dir/skills" ]]; then
    validate_payload_tree "$payload_dir/skills" "skills" || return 1
  fi
  stage="$(mktemp -d "$STATE_DIR/.download-stage.XXXXXX")" || return 1
  if ! cp -f "$src" "$stage/bootstrap.sh"; then
    rm -rf "$stage"
    return 1
  fi
  chmod 755 "$stage/bootstrap.sh"

  # W6.5 scripts must travel with the canonical copy, or
  # `bootstrap.sh --composio/--hermes-stack/--verify-stack` from
  # ~/.aios-core/download would BLOCK with script-missing after the DMG
  # is ejected (W65_DIR resolves relative to BASH_SOURCE).
  if [[ -d "$payload_dir/scripts" ]]; then
    cp -R "$payload_dir/scripts" "$stage/scripts" || { rm -rf "$stage"; return 1; }
    find "$stage/scripts" -name '*.sh' -exec chmod 755 {} \;
  fi
  if [[ -d "$payload_dir/skills" ]]; then
    cp -R "$payload_dir/skills" "$stage/skills" || { rm -rf "$stage"; return 1; }
  fi
  if [[ -f "$payload_dir/fleet-checksums.sha256" && ! -L "$payload_dir/fleet-checksums.sha256" ]]; then
    cp -f "$payload_dir/fleet-checksums.sha256" "$stage/fleet-checksums.sha256" \
      || { rm -rf "$stage"; return 1; }
  fi

  if ! promote_download_stage "$stage"; then
    rm -rf "$stage"
    return 1
  fi
}

browser_complete_url() {
  printf '%s/onboarding?path=restore&restore=%s&bootstrap=complete&done=1&step=3' "$HOSTED_BASE_URL" "$RESTORE_MODE"
}
browser_blocked_url() {
  printf '%s/onboarding?path=restore&restore=%s&blocked=%s&step=3' "$HOSTED_BASE_URL" "$RESTORE_MODE" "$1"
}

# Policy boundary per restore mode: what this installer will NOT copy to the
# target machine. Surfaced in state.json, RESTORE_COMPLETE, INSTALL_SUMMARY,
# and the redacted install report so a clone can prove what it excluded.
restore_mode_exclusions() {
  case "$RESTORE_MODE" in
    core-stack) printf '%s' "secrets, sessions, personal memories, private corpora, active board state, logs, app sessions, generated documents" ;;
    owner-restore) printf '%s' "secret values in output (owner-approved selective restore via 1Password prompts; values are never printed)" ;;
    contractor-va-workstation) printf '%s' "private SSH keys (public keys only), owner session history, personal memories, owner secrets, unsanitized knowledge" ;;
    client-operator-machine) printf '%s' "owner personal data, private repos, memories, secrets (credentials supplied by the client/operator)" ;;
    # Unreachable today (RESTORE_MODE is validated at argument parse) — but a
    # policy-bearing function gets a defensive default: unknown modes fall back
    # to the most restrictive (core-stack) exclusion set. (§13.3 board finding)
    *) printf '%s' "unknown mode — treated as core-stack exclusions: secrets, sessions, personal memories, private corpora, active board state, logs, app sessions, generated documents" ;;
  esac
}


backup_guard_fresh() {
  # Ready-to-clone must mean a current, restorable state exists — not just that scripts are installed.
  # Accepts only a green backup-guard receipt newer than 26h and a hermes-state archive newer than 26h.
  local receipt archive now max_age newest_age archive_age status fails
  now="$(date -u +%s)"
  max_age=$((26 * 60 * 60))
  receipt="$(ls -t "$STATE_DIR"/receipts/backup-guard-*.json 2>/dev/null | head -1)"
  [ -n "$receipt" ] || return 1
  status="$(grep -o '"status":"[^"]*"' "$receipt" 2>/dev/null | head -1 | cut -d'"' -f4)"
  fails="$(grep -o '"fails":"[^"]*"' "$receipt" 2>/dev/null | head -1 | cut -d'"' -f4)"
  [ "$status" = "green" ] && [ -z "$fails" ] || return 1
  newest_age=$((now - $(stat -f %m "$receipt" 2>/dev/null || echo 0)))
  [ "$newest_age" -le "$max_age" ] || return 1
  archive="$(ls -t "$HOME/backups/hermes-state"/hermes-state-*.tar.gz 2>/dev/null | head -1)"
  [ -n "$archive" ] || return 1
  tar -tzf "$archive" >/dev/null 2>&1 || return 1
  archive_age=$((now - $(stat -f %m "$archive" 2>/dev/null || echo 0)))
  [ "$archive_age" -le "$max_age" ] || return 1
}

# ── Gate detection (single source of truth for wizard + dashboard) ──
GATE_IDS="xcode-tools homebrew-path 1password-app 1password-cli 1password-auth github-cli github-auth docker-cli orbstack-runtime backup-freshness"
gate_status() {
  case "$1" in
    xcode-tools)      xcode-select -p >/dev/null 2>&1 && echo pass || echo pending ;;
    homebrew-path)    { have brew || [[ -x /opt/homebrew/bin/brew || -x /usr/local/bin/brew ]]; } && echo pass || echo pending ;;
    1password-app)    [[ -d "/Applications/1Password.app" ]] && echo pass || echo pending ;;
    1password-cli)    have op && echo pass || echo pending ;;
    1password-auth)   op account list >/dev/null 2>&1 && echo pass || echo pending ;;
    github-cli)       have gh && echo pass || echo pending ;;
    github-auth)      gh auth status >/dev/null 2>&1 && echo pass || echo pending ;;
    docker-cli)       have docker && echo pass || echo pending ;;
    orbstack-runtime) orbstack_docker_ready && echo pass || echo pending ;;
    backup-freshness) backup_guard_fresh >/dev/null 2>&1 && echo pass || echo pending ;;
    *) echo pending ;;
  esac
}
gates_json() {
  local out="" id st ts first=1
  ts="$(now_utc)"
  for id in $GATE_IDS; do
    st="$(gate_status "$id")"
    if [[ $first -eq 1 ]]; then first=0; else out="$out,"; fi
    out="$out
    {\"id\": $(jstr "$id"), \"status\": $(jstr "$st"), \"checkedAt\": $(jstr "$ts")}"
  done
  printf '%s\n  ' "$out"
}
gate_order_json() {
  # Postflight gate order as an explicit array so UIs can show operators which
  # gates precede backup-freshness without inferring from gates[]. (§13.3)
  local out="" id first=1
  for id in $GATE_IDS; do
    if [[ $first -eq 1 ]]; then first=0; else out="$out, "; fi
    out="$out$(jstr "$id")"
  done
  printf '%s' "$out"
}

# ── Atomic state writers (temp + mv) ──
write_state_file() {
  # $1=status $2=phase $3=blockedBy(or empty) $4=headline $5=message
  # $6=nextLabel(or empty) $7=nextCommand(or empty) $8=browserUrl $9=proofFilesJson
  local tmp="$STATE_FILE.tmp.$$" now next_json blocked_json
  now="$(now_utc)"
  [[ -n "$STARTED_AT" ]] || STARTED_AT="$now"
  if [[ -n "${7:-}" ]]; then
    next_json="{
    \"label\": $(jstr "$6"),
    \"command\": $(jstr "$7")
  }"
  else
    next_json="null"
  fi
  if [[ -n "${3:-}" ]]; then blocked_json="$(jstr "$3")"; else blocked_json="null"; fi
  cat >"$tmp" <<JSON
{
  "schemaVersion": 2,
  "workerVersion": $(jstr "$WORKER_VERSION"),
  "profile": $(jstr "$PROFILE"),
  "restoreMode": $(jstr "$RESTORE_MODE"),
  "excludedDataClasses": $(jstr "$(restore_mode_exclusions)"),
  "installRoot": $(jstr "$INSTALL_ROOT"),
  "manifest": $(jstr "$MANIFEST_DEST"),
  "startedAt": $(jstr "$STARTED_AT"),
  "updatedAt": $(jstr "$now"),
  "status": $(jstr "$1"),
  "phase": $(jstr "$2"),
  "workerPid": $$,
  "workerBootTime": $(boot_epoch),
  "blockedBy": $blocked_json,
  "headline": $(jstr "$4"),
  "message": $(jstr "$5"),
  "nextAction": $next_json,
  "browserUrl": $(jstr "$8"),
  "proofFiles": [${9:-}],
  "packetReceipt": $(jstr "$STATE_DIR/receipts/packet-applied.json"),
  "localDashboardPath": $(jstr "$INSTALL_ROOT/dashboard"),
  "localLauncher": $(jstr "$INSTALL_ROOT/start-aios-dashboard.sh"),
  "gateOrder": [$(gate_order_json)],
  "gates": [$(gates_json)]
}
JSON
  mv -f "$tmp" "$STATE_FILE"
}

write_state_running() {
  CURRENT_PHASE="$1"
  write_state_file "running" "$1" "" "AI-OS CORE INSTALL RUNNING" "Phase: $1" "" "" "" "" || true
}

write_next_step() {
  cat > "$NEXT_STEP_FILE" <<NEXT
AI-OS Core next required action

COPY ONLY THE COMMAND BETWEEN THE BRACKETS:
[
$1
]

Do not copy the bracket lines. Do not type status/detail lines from the installer.
NEXT
}

write_state_complete() {
  local now url
  now="$(now_utc)"
  url="$(browser_complete_url)"
  rm -f "$NEXT_STEP_FILE"
  cat >"$COMPLETE_FILE" <<DONE
AI-OS Core restore/bootstrap complete
profile=$PROFILE
restore_mode=$RESTORE_MODE
excluded_data_classes=$(restore_mode_exclusions)
install_root=$INSTALL_ROOT
manifest=$MANIFEST_DEST
completed_at=$now
DONE
  cat >"$SUMMARY_FILE" <<SUMMARY
AI-OS CORE RESTORE IS COMPLETE.

Restore mode: $RESTORE_MODE
Excluded by this mode: $(restore_mode_exclusions)

What is done:
- Fresh-Mac prerequisites are installed or verified.
- Required postflight gates passed, including backup freshness before ready-to-clone.
- AI-OS Core install root exists: $INSTALL_ROOT
- Public AI-OS Core packet is downloaded, verified, and applied.
- Packet receipt is saved: $STATE_DIR/receipts/packet-applied.json
- Local dashboard source is downloaded: $INSTALL_ROOT/dashboard
- Local dashboard launcher is installed: $INSTALL_ROOT/start-aios-dashboard.sh
- Dashboard dependencies install on first local launch if node_modules is missing.
- Hosted manifest is saved: $MANIFEST_DEST
- Completion proof is saved: $COMPLETE_FILE

You do not need to run another restore command.
To open the local application, run: bash $INSTALL_ROOT/start-aios-dashboard.sh
Use setup cards only for optional account/app integrations.
SUMMARY
  write_state_file "complete" "complete" "" "AI-OS CORE RESTORE COMPLETE" \
    "You are done with the Core restore/bootstrap phase." "" "" "$url" \
    "$(jstr "$COMPLETE_FILE"), $(jstr "$SUMMARY_FILE"), $(jstr "$STATE_DIR/receipts/packet-applied.json")"
}

write_state_blocked() {
  local id="$1" message="$2" command="$3" label="${4:-Fix blocker and rerun}" url
  url="$(browser_blocked_url "$id")"
  rm -f "$COMPLETE_FILE"
  write_next_step "$command"
  cat >"$SUMMARY_FILE" <<SUMMARY
AI-OS CORE RESTORE IS BLOCKED.

Blocker: $id
$message

Required next command:
[
$command
]

After the blocker is complete, launch AI-OS Core Installer again. Do not rerun this blocked command in a loop.
SUMMARY
  write_state_file "blocked" "$CURRENT_PHASE" "$id" "AI-OS CORE RESTORE BLOCKED: $id" \
    "$message" "$label" "$command" "$url" \
    "$(jstr "$STATE_FILE"), $(jstr "$NEXT_STEP_FILE"), $(jstr "$SUMMARY_FILE")"
}

print_complete() {
  printf '\n========================================\n'
  printf 'AI-OS CORE RESTORE COMPLETE\n'
  printf '========================================\n'
  printf 'You are done with the restore/bootstrap phase.\n'
  printf 'Proof file: %s\n' "$COMPLETE_FILE"
  printf 'Summary file: %s\n' "$SUMMARY_FILE"
  printf 'State file: %s\n' "$STATE_FILE"
  printf '\nNo Terminal command is required.\n'
  printf 'Local AI-OS Dashboard files: %s/dashboard\n' "$INSTALL_ROOT"
  printf 'Launcher: %s/start-aios-dashboard.sh\n' "$INSTALL_ROOT"
  printf 'OPTIONAL visual receipt: %s\n' "$(browser_complete_url)"
  printf '\nOPEN LOCAL AI-OS DASHBOARD - COPY ONLY THE COMMAND BETWEEN THE BRACKETS\n'
  printf '[\nbash %q\n]\n' "$INSTALL_ROOT/start-aios-dashboard.sh"
  printf '\nVIEW SUMMARY - COPY ONLY THE COMMAND BETWEEN THE BRACKETS\n'
  printf '[\ncat %q\n]\n' "$SUMMARY_FILE"
  generate_report
}

print_blocked() {
  local id="$1" command="$2"
  printf '\n========================================\n'
  printf 'AI-OS CORE RESTORE BLOCKED: %s\n' "$id"
  printf '========================================\n'
  printf 'Required next action is saved here: %s\n' "$NEXT_STEP_FILE"
  printf 'State file: %s\n' "$STATE_FILE"
  printf '\nNEXT ACTION - COPY ONLY THE COMMAND BETWEEN THE BRACKETS\n'
  printf '[\n%s\n]\n' "$command"
  printf '\nDo not rerun this blocked command in a loop. Complete the named blocker, then launch AI-OS Core Installer again.\n'
}

block_and_exit() {
  trap - ERR
  local id="$1" message="$2" command="$3" label="${4:-Fix blocker}"
  write_state_blocked "$id" "$message" "$command" "$label"
  print_blocked "$id" "$command"
  generate_report
  exit 1
}

complete_and_exit() {
  trap - ERR
  write_state_complete
  print_complete
  exit 0
}

# ── ERR trap: any unanticipated failure still writes honest state (audit B2) ──
on_err() {
  local exit_code=$? line=${BASH_LINENO[0]:-?} cmd=${BASH_COMMAND:-?}
  # set -E propagates this trap into $() subshells (e.g. the gate probes that
  # gates_json runs under pipefail while state.json is mid-write). A subshell
  # ERR is not a terminal installer outcome — and writing state here would
  # reuse the parent's $$-named tmp file and rename state.json.tmp out from
  # under the parent's atomic write (fresh-Mac first-run failure).
  if [ "${BASH_SUBSHELL:-0}" -gt 0 ]; then
    exit "$exit_code"
  fi
  trap - ERR
  write_state_blocked "unexpected-error" \
    "The installer hit an unexpected failure (exit $exit_code at line $line: $cmd). This is safe to retry." \
    "$RERUN_CMD" \
    "Retry the installer" || true
  print_blocked "unexpected-error" "$RERUN_CMD" || true
  exit "$exit_code"
}
trap on_err ERR

# --plan is a dry run: it reports what a real run WOULD do and writes NOTHING.
#
# Purity is the whole contract. --scan is the cautionary example: it looks like
# detection but creates $STATE_DIR and rewrites state.json before exiting, and
# it is dispatched before self_copy, so no --scan ever refreshes the durable
# copy. --plan must never become an alias of it. Everything below reads only:
# no mkdir, no state file, no sample, no download.
#
# It reports rather than refuses. Under PAUSE a real run would stop, and the
# plan says so -- exiting 4 here would defeat the point of asking.
plan_line() { printf '  %-22s %s\n' "$1" "$2"; }
plan_report() {
  local id st would phase_note
  printf 'AI-OS Core install plan (dry run -- nothing on this Mac is changed)\n\n'
  printf 'Target\n'
  plan_line "profile"      "$PROFILE"
  plan_line "restore mode" "$RESTORE_MODE"
  plan_line "excludes"     "$(restore_mode_exclusions)"
  plan_line "install root" "$INSTALL_ROOT"
  plan_line "state dir"    "$STATE_DIR"
  plan_line "source"       "$HOSTED_BASE_URL"

  printf '\nDisk\n'
  if disk_evaluate 0; then
    case "$DISK_BAND" in
      warn-low)      plan_line "verdict" "WARN: ${DISK_FREE} GiB free, under the ${DISK_WARN_GIB} GiB comfort floor" ;;
      warn-velocity) plan_line "verdict" "WARN: filling fast, about ${DISK_VELOCITY}h to empty" ;;
      *)             plan_line "verdict" "OK: ${DISK_FREE} GiB free" ;;
    esac
    plan_line "would proceed" "yes"
  else
    plan_line "verdict" "PAUSE: ${DISK_FREE} GiB free, ${DISK_REQUIRED} GiB required"
    plan_line "would proceed" "NO -- the install would refuse until space is freed"
  fi

  printf '\nGates detected now\n'
  for id in $GATE_IDS; do
    st="$(gate_status "$id")"
    plan_line "$id" "$st"
  done

  printf '\nPhases a --install-tools run would perform\n'
  if [ -f "$COMPLETE_FILE" ]; then
    plan_line "overall" "already complete -- a re-run repairs and re-verifies only"
  fi
  plan_line "install-tools" "run: install or repair Xcode CLT, Homebrew, casks (already-present items are skipped)"
  if [ -f "$STATE_DIR/receipts/packet-applied.json" ]; then
    would="skip: packet receipt already present (re-applied only if the packet changed)"
  else
    would="run: download, verify by sha256, then apply the core packet"
  fi
  plan_line "packet" "$would"
  if [ -f "$INSTALL_ROOT/dashboard/package.json" ]; then
    would="skip: dashboard checkout already present"
  else
    would="run: clone the dashboard app"
  fi
  plan_line "dashboard" "$would"
  plan_line "postflight" "run: re-check every gate and write the terminal outcome"

  printf '\nPredicted outcome\n'
  if [ "$DISK_BAND" = "pause" ]; then
    plan_line "would end" "BLOCKED: disk-pressure"
  else
    phase_note="COMPLETE"
    for id in $GATE_IDS; do
      if [ "$(gate_status "$id")" != "pass" ]; then phase_note="BLOCKED: $id (first failing gate)"; break; fi
    done
    plan_line "would end" "$phase_note"
  fi
  printf '\nThis is a prediction from detection performed just now, not a guarantee.\n'
  printf 'Run the install for real with:\n[\n%s\n]\n' "$(disk_rerun_cmd)"
}

if [[ "$PLAN_ONLY" == "1" ]]; then
  plan_report
  trap - ERR
  exit 0
fi

# --disk-check is the pure diagnostic: it measures, reports, and writes NOTHING
# (no state dir, no sample, no state.json). Dispatched first so it stays usable
# on a full disk -- rule 10 requires diagnosis to survive PAUSE, since a guard
# that locks you out of fixing the problem is worse than no guard. Contrast
# --scan below, which does create $STATE_DIR and write state.json before exiting.
if [[ "$DISK_CHECK" == "1" ]]; then
  disk_gate 0 "this Mac"
  disk_free_now="$(disk_free_gib)"
  disk_vhours="$(disk_velocity_hours)"
  # Report the band the gate actually decided. Printing a flat "OK" here made
  # --disk-check contradict its own stderr warning in the WARN band.
  case "$DISK_BAND" in
    warn-low)      disk_verdict="WARN (below the comfort floor)" ;;
    warn-velocity) disk_verdict="WARN (filling fast)" ;;
    *)             disk_verdict="OK" ;;
  esac
  printf 'disk: %s (%s GiB free; warn under %s GiB, pause under %s GiB)\n' \
    "$disk_verdict" "$disk_free_now" "$DISK_WARN_GIB" "$DISK_PAUSE_GIB"
  if [ -n "$disk_vhours" ]; then
    printf 'disk: projected time-to-empty about %sh (velocity floor %sh)\n' \
      "$disk_vhours" "$DISK_VELOCITY_HOURS"
  else
    printf 'disk: velocity unknown (needs two samples over at least 5 minutes)\n'
  fi
  trap - ERR
  exit 0
fi

# ── --scan blocked-state preservation ───────────────────────────────────────
# A scan may refresh what it DETECTS (gates) and must not destroy what it did
# not re-derive (the blocked outcome). The Blocked screen's [Check again] button
# calls --scan, so overwriting blockedBy/message/nextAction discarded the very
# blocker the button exists to re-check, leaving the user with no action.
scan_state_field() {
  [ -f "$STATE_FILE" ] || return 0
  sed -n "s/^  \"$1\": \"\([^\"]*\)\".*\$/\1/p" "$STATE_FILE" | head -1
}

# Only a gate-backed blocker can be proven resolved by detection alone. A failed
# download or an unexpected error needs a real run before it can be cleared.
scan_blocker_cleared() {
  [ -n "${1:-}" ] || return 1
  case " $GATE_IDS " in
    *" $1 "*) [ "$(gate_status "$1")" = "pass" ] ;;
    *) return 1 ;;
  esac
}

# Refresh ONLY updatedAt and gates[], passing every other field through
# verbatim. The recorded message and nextAction are already JSON-escaped, so
# re-serialising them through jstr would double-escape any embedded quote (the
# 1password-auth command carries two). Their bytes are never round-tripped.
scan_refresh_blocked() {
  local tmp="$STATE_FILE.tmp.$$" now
  now="$(now_utc)"
  {
    awk -v now="$now" '
      /^  "updatedAt":/ { printf "  \"updatedAt\": \"%s\",\n", now; next }
      /^  "gates": \[/  { exit }
      { print }
    ' "$STATE_FILE"
    printf '  "gates": [%s]\n}\n' "$(gates_json)"
  } > "$tmp" 2>/dev/null && mv -f "$tmp" "$STATE_FILE" || { rm -f "$tmp"; return 1; }
}

# --scan is intentionally dispatched before install-root creation, self-copy,
# W6.5 refresh, or v1 migration. Its only permitted write is the documented
# gate/state refresh under ~/.aios-core.
if [[ "$SCAN_ONLY" == "1" ]]; then
  if [[ -L "$STATE_DIR" ]]; then
    echo "AI-OS CORE RESTORE BLOCKED: state-dir-symlink" >&2
    exit 4
  fi
  mkdir -p "$STATE_DIR"
  scan_prev_status="$(scan_state_field status)"
  scan_prev_blocker="$(scan_state_field blockedBy)"
  if [[ -f "$COMPLETE_FILE" ]]; then
    write_state_file "complete" "complete" "" "AI-OS CORE RESTORE COMPLETE" \
      "Scan refresh: completion proof present." "" "" "$(browser_complete_url)" \
      "$(jstr "$COMPLETE_FILE"), $(jstr "$SUMMARY_FILE"), $(jstr "$STATE_DIR/receipts/packet-applied.json")"
  elif [[ "$scan_prev_status" == "blocked" ]] && ! scan_blocker_cleared "$scan_prev_blocker"; then
    # Still blocked, and detection cannot prove otherwise: keep the outcome and
    # refresh only the checklist. A failure here falls through to a plain scan
    # rather than leaving a half-written state file.
    scan_refresh_blocked \
      || write_state_file "running" "scan" "" "AI-OS CORE SCAN" "Gate scan refreshed; no install action taken." "" "" "" ""
  else
    # Either nothing was blocked, or the recorded blocker is a gate that now
    # passes -- [Check again] after fixing the problem clears the block.
    write_state_file "running" "scan" "" "AI-OS CORE SCAN" "Gate scan refreshed; no install action taken." "" "" "" ""
  fi
  [[ "$JSON_STATE" == "1" ]] && cat "$STATE_FILE"
  trap - ERR
  exit 0
fi

# Gate BEFORE the first mutation. Everything above this line is read-only
# preflight; the mkdir below is where this worker starts changing the machine.
# On a fresh Mac ~/.hermes/scripts/disk-preflight.sh does not exist yet -- this
# installer is what installs it -- so the thresholds are inlined, exactly as
# scripts/install-companions/preflight.sh already inlines MIN_FREE_GB.
#
# Diagnostic modes are deliberately exempt. Rule 10 requires diagnosis and
# cleanup to stay available under PAUSE, because a guard that locks the user out
# of fixing a full disk is worse than no guard at all -- and --report is exactly
# what support asks for when an install fails on a full disk. None of these
# modes performs a bulk write.
if [ "${REPORT_ONLY:-0}" != "1" ] && [ "${KEYS_STATUS:-0}" != "1" ] && [ "${VERIFY_STACK:-0}" != "1" ]; then
  disk_gate 0 "the AI-OS Core install"
fi

mkdir -p "$INSTALL_ROOT" "$INSTALL_ROOT/recovery" "$INSTALL_ROOT/dashboard" "$STATE_DIR" "$STATE_DIR/receipts"
disk_record_sample
export HOMEBREW_NO_AUTO_UPDATE="${HOMEBREW_NO_AUTO_UPDATE:-1}"
export HOMEBREW_NO_INSTALL_CLEANUP="${HOMEBREW_NO_INSTALL_CLEANUP:-1}"

# ── W6.5: Hermes stack / Composio modes (exec bundled scripts, exit with their code) ──
W65_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/scripts"
# dd02mini W8 field finding #5: when running from the canonical download copy,
# scripts/ can be STALE (self_copy snapshot from an older DMG/bootstrap). Before
# refreshing them, prove the durable bootstrap is exactly the hosted release.
# The hosted origin is never allowed to replace or execute bootstrap code.
verify_hosted_bootstrap_version() {
  local self_path hosted_sum local_sum
  self_path="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/$(basename "${BASH_SOURCE[0]}")"
  [[ "$self_path" == "$CANONICAL_SELF" ]] || return 0
  [[ -f "$self_path" && ! -L "$self_path" ]] || {
    echo "[bootstrap] BLOCKED: durable bootstrap is not a regular file" >&2
    return 1
  }
  case "$HOSTED_BASE_URL" in
    https://*) ;;
    *) echo "[bootstrap] BLOCKED: hosted base URL must use HTTPS" >&2; return 1 ;;
  esac
  hosted_sum="$(curl -fsSL --proto '=https' --proto-redir '=https' --tlsv1.2 --max-time 10 \
    "$HOSTED_BASE_URL/aios-core/bootstrap.sh.sha256" 2>/dev/null | awk 'NR == 1 { print $1 }')" || {
    echo "[bootstrap] BLOCKED: could not fetch the hosted bootstrap checksum" >&2
    return 1
  }
  [[ "$hosted_sum" =~ ^[a-f0-9]{64}$ ]] || {
    echo "[bootstrap] BLOCKED: hosted bootstrap checksum is invalid" >&2
    return 1
  }
  local_sum="$(shasum -a 256 "$self_path" | awk '{print $1}')"
  if [[ "$hosted_sum" != "$local_sum" ]]; then
    echo "[bootstrap] BLOCKED: durable bootstrap version differs from the hosted release" >&2
    echo "[bootstrap] Re-download the signed installer/bootstrap; automatic code replacement is disabled" >&2
    return 1
  fi
}

FLEET_MANIFEST_SHA256="36baef64fc0fd9b5c013baae050cbba0d6eff6738fd2c2ce3117a21d876f2ccc"
refresh_w65_scripts() {
  verify_hosted_bootstrap_version || return 1
  local self_dir base="$HOSTED_BASE_URL/aios-core" stage line expected rel actual
  self_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
  if [[ "$self_dir" != "$STATE_DIR/download" ]]; then
    # DMG / self-copied payload runs carry their own verified scripts/ next to
    # this file — keep them (offline-capable). A FRESH standalone download (the
    # site one-liners: curl bootstrap.sh && bash bootstrap.sh --<flag>) has no
    # sibling scripts/, so stage the pinned fleet into the durable dir exactly
    # like a durable-dir refresh, then point W65_DIR at the verified copy.
    [[ -d "$self_dir/scripts" ]] && return 0
    mkdir -p "$STATE_DIR"
  fi
      stage="$(mktemp -d "$STATE_DIR/.download-stage.XXXXXX")" || return 1
      if ! cp -f "$self_dir/$(basename "${BASH_SOURCE[0]}")" "$stage/bootstrap.sh"; then
        rm -rf "$stage"
        echo "[bootstrap] BLOCKED: durable bootstrap could not be staged" >&2
        return 1
      fi
      chmod 755 "$stage/bootstrap.sh"
      if ! curl -fsSL --proto '=https' --proto-redir '=https' --tlsv1.2 --max-time 20 \
        "$base/fleet-checksums.sha256" -o "$stage/fleet-checksums.sha256"; then
        rm -rf "$stage"
        echo "[bootstrap] BLOCKED: fleet manifest download failed" >&2
        return 1
      fi
      actual="$(shasum -a 256 "$stage/fleet-checksums.sha256" | awk '{print $1}')"
      if [[ "$actual" != "$FLEET_MANIFEST_SHA256" ]]; then
        rm -rf "$stage"
        echo "[bootstrap] BLOCKED: fleet manifest pin mismatch" >&2
        return 1
      fi
      while IFS= read -r line || [[ -n "$line" ]]; do
        [[ -z "$line" || "$line" == \#* ]] && continue
        expected="${line%%  *}"
        rel="${line#*  }"
        if [[ ! "$expected" =~ ^[a-f0-9]{64}$ ]] \
          || [[ "$rel" == /* || "$rel" == *".."* ]] \
          || [[ "$rel" != scripts/* && "$rel" != "skills/chat-knowledge-capture/SKILL.md" ]]; then
          rm -rf "$stage"
          echo "[bootstrap] BLOCKED: fleet manifest contains an unsafe entry" >&2
          return 1
        fi
        mkdir -p "$stage/$(dirname "$rel")"
        if ! curl -fsSL --proto '=https' --proto-redir '=https' --tlsv1.2 --max-time 20 \
          "$base/$rel" -o "$stage/$rel"; then
          rm -rf "$stage"
          echo "[bootstrap] BLOCKED: fleet member download failed: $rel" >&2
          return 1
        fi
      done < "$stage/fleet-checksums.sha256"
      if ! (cd "$stage" && shasum -a 256 -c fleet-checksums.sha256 --status --strict); then
        rm -rf "$stage"
        echo "[bootstrap] BLOCKED: fleet generation checksum verification failed" >&2
        return 1
      fi
      find "$stage/scripts" -name '*.sh' -exec chmod 755 {} \;
      find "$stage/scripts" -type f ! -name '*.sh' -exec chmod 644 {} \;
      find "$stage/skills" -type f -exec chmod 644 {} \;
      if ! promote_download_stage "$stage"; then
        rm -rf "$stage"
        echo "[bootstrap] BLOCKED: verified fleet generation was not promoted" >&2
        return 1
      fi
      if [[ "$self_dir" != "$STATE_DIR/download" ]]; then
        W65_DIR="$STATE_DIR/download/scripts"
        log_w65 "fresh download: using verified fleet at $W65_DIR"
      fi
      log_w65 "verified fleet generation refreshed from $base"
}
log_w65() { printf '[bootstrap] %s\n' "$*"; }
if [[ "${REPORT_ONLY:-0}" == "1" ]]; then
  generate_report
  echo "Report written. COPY ONLY THE COMMAND BETWEEN THE BRACKETS to view it:"
  printf '[\nopen ~/.aios-core/INSTALL_REPORT.txt\n]\n'
  exit 0
fi
if [[ "${HERMES_STACK:-0}" == "1" ]]; then
  refresh_w65_scripts
  [[ -f "$W65_DIR/install-hermes-stack.sh" ]] || { echo "AI-OS CORE RESTORE BLOCKED: hermes-script-missing" >&2; exit 4; }
  # --update applies generic append-only improvements to an existing install
  # (display block, bundled skills an older install predates) without a clean
  # machine — no uninstall. Personalized config is preserved (only-if-absent writes).
  if [[ "${UPDATE_STACK:-0}" == "1" ]]; then
    exec bash "$W65_DIR/install-hermes-stack.sh" --update
  fi
  exec bash "$W65_DIR/install-hermes-stack.sh"
fi
if [[ "${VERIFY_STACK:-0}" == "1" ]]; then
  refresh_w65_scripts
  [[ -f "$W65_DIR/verify-installed-topology.sh" ]] || { echo "AI-OS CORE RESTORE BLOCKED: verify-script-missing" >&2; exit 4; }
  exec bash "$W65_DIR/verify-installed-topology.sh"
fi
if [[ "${BACKUP_GUARD:-0}" == "1" ]]; then
  refresh_w65_scripts
  [[ -f "$W65_DIR/install-companions/backup-guard.sh" ]] || { echo "AI-OS CORE RESTORE BLOCKED: backup-guard-script-missing" >&2; exit 4; }
  exec bash "$W65_DIR/install-companions/backup-guard.sh"
fi

if [[ "${KEYS_STATUS:-0}" == "1" ]]; then
  refresh_w65_scripts
  [[ -f "$W65_DIR/install-companions/keys-status.sh" ]] || { echo "AI-OS CORE RESTORE BLOCKED: keys-status-script-missing" >&2; exit 4; }
  exec bash "$W65_DIR/install-companions/keys-status.sh"
fi

if [[ "${RESTORE_KEYS:-0}" == "1" ]]; then
  refresh_w65_scripts
  [[ -f "$W65_DIR/install-companions/restore-keys.sh" ]] || { echo "AI-OS CORE RESTORE BLOCKED: restore-keys-script-missing" >&2; exit 4; }
  exec bash "$W65_DIR/install-companions/restore-keys.sh"
fi

if [[ "${CONNECT_STACK:-0}" == "1" ]]; then
  refresh_w65_scripts
  [[ -f "$W65_DIR/install-companions/connect-stack.sh" ]] || { echo "AI-OS CORE RESTORE BLOCKED: connect-stack-script-missing" >&2; exit 4; }
  exec bash "$W65_DIR/install-companions/connect-stack.sh"
fi

if [[ "${COMPANIONS:-0}" == "1" ]]; then
  refresh_w65_scripts
  [[ -f "$W65_DIR/install-companions/install-companions.sh" ]] || { echo "AI-OS CORE RESTORE BLOCKED: companions-script-missing" >&2; exit 4; }
  exec bash "$W65_DIR/install-companions/install-companions.sh" "$COMPANIONS_ONLY"
fi
if [[ "${OMEGA:-0}" == "1" ]]; then
  # Generic OMEGA persistent-memory capability for Claude Code (venv + pip pkg +
  # append-only MCP/hook registration + fresh per-machine store). No personal data.
  refresh_w65_scripts
  [[ -f "$W65_DIR/install-companions/install-omega.sh" ]] || { echo "AI-OS CORE RESTORE BLOCKED: omega-script-missing" >&2; exit 4; }
  exec bash "$W65_DIR/install-companions/install-omega.sh"
fi
if [[ "${TERMINAL:-0}" == "1" ]]; then
  # Recreate the AI-OS classic terminal environment for the CURRENT user: Ghostty
  # + font casks, generic ${HOME}-relative tmux/ghostty configs + helper scripts.
  # Idempotent + only-if-absent (never clobbers an existing dotfile).
  refresh_w65_scripts
  [[ -f "$W65_DIR/install-companions/install-terminal-ux.sh" ]] || { echo "AI-OS CORE RESTORE BLOCKED: terminal-script-missing" >&2; exit 4; }
  exec bash "$W65_DIR/install-companions/install-terminal-ux.sh"
fi

if [[ "${PROVISION:-0}" == "1" ]]; then
  # One resumable, gated run of the whole new-Mac sequence. Every stage records
  # its outcome; a failure stops with exactly one next command; re-running skips
  # what already succeeded.
  refresh_w65_scripts
  [[ -f "$W65_DIR/provision.sh" ]] || { echo "AI-OS CORE RESTORE BLOCKED: provision-script-missing" >&2; exit 4; }
  exec bash "$W65_DIR/provision.sh"
fi

if [[ "${SYNC_PRIVATE:-0}" == "1" ]]; then
  # Pull owner-only material (skills, scripts, personal-os) straight from a Mac
  # you already control, over your own SSH/Tailscale link. The public install
  # site never serves this content. Credentials are excluded by rule; restore
  # them with --restore-keys. Previews by default (AIOS_SYNC_APPLY=1 to write).
  refresh_w65_scripts
  [[ -f "$W65_DIR/install-companions/sync-private-stack.sh" ]] || { echo "AI-OS CORE RESTORE BLOCKED: sync-private-script-missing" >&2; exit 4; }
  exec bash "$W65_DIR/install-companions/sync-private-stack.sh"
fi

if [[ "${COMPOSIO:-0}" == "1" ]]; then
  refresh_w65_scripts
  [[ -f "$W65_DIR/install-composio-connect.sh" ]] || { echo "AI-OS CORE RESTORE BLOCKED: composio-script-missing" >&2; exit 4; }
  exec bash "$W65_DIR/install-composio-connect.sh"
fi

# ── v1 → v2 migration (idempotent) ──
migrate_v1_state() {
  if [[ -f "$STATE_FILE" ]] && ! grep -q '"schemaVersion": 2' "$STATE_FILE" 2>/dev/null; then
    local bak="$HOME/.aios-core.v1-backup-$(date +%Y%m%d%H%M%S)"
    cp -R "$STATE_DIR" "$bak" 2>/dev/null || true
    log "Migrated v1 state (backup: $bak)"
  fi
  # Reconcile mixed-generation state: stale NEXT_STEP beside a COMPLETE proof.
  if [[ -f "$COMPLETE_FILE" && -f "$NEXT_STEP_FILE" ]]; then
    rm -f "$NEXT_STEP_FILE"
  fi
}

self_copy
migrate_v1_state

brew_bin() {
  if have brew; then command -v brew; return 0; fi
  if [[ -x /opt/homebrew/bin/brew ]]; then printf '/opt/homebrew/bin/brew\n'; return 0; fi
  if [[ -x /usr/local/bin/brew ]]; then printf '/usr/local/bin/brew\n'; return 0; fi
  return 1
}
load_brew() {
  local brew_path
  if brew_path="$(brew_bin)"; then eval "$("$brew_path" shellenv)"; return 0; fi
  return 1
}
persist_brew_shellenv() {
  local brew_path="$1" zprofile="$HOME/.zprofile" line
  line="eval \"\$($brew_path shellenv)\""
  touch "$zprofile"
  if ! grep -Fq "$line" "$zprofile"; then
    printf '\n# Homebrew PATH for AI-OS bootstrap\n%s\n' "$line" >> "$zprofile"
  fi
}
install_homebrew_if_needed() {
  if load_brew; then
    log "Homebrew found: $(command -v brew)"
    persist_brew_shellenv "$(command -v brew)"
    return 0
  fi
  log "Homebrew is missing. Installing Homebrew now."
  /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" \
    || block_and_exit "homebrew-install" "The Homebrew installer failed (often a network blip)." "$RERUN_CMD" "Retry the installer"
  if ! load_brew; then
    block_and_exit "homebrew-path" "Homebrew installer finished, but brew is not on PATH in this Terminal." "exec zsh -l -c '$RERUN_CMD'" "Reload shell and rerun bootstrap"
  fi
  persist_brew_shellenv "$(command -v brew)"
}

install_xcode_clt_if_needed() {
  if xcode-select -p >/dev/null 2>&1; then
    log "Xcode Command Line Tools found: $(xcode-select -p)"
    return 0
  fi
  # No-TTY rule: a wizard-spawned worker can never do interactive sudo.
  if [[ -t 0 ]]; then
    log "Xcode Command Line Tools are missing. Trying non-interactive Apple Software Update first."
    local marker label
    marker="/tmp/.com.apple.dt.CommandLineTools.installondemand.in-progress"
    touch "$marker" 2>/dev/null || true
    label="$(softwareupdate -l 2>/dev/null | sed -n 's/^.*\* Label: \(Command Line Tools.*\)$/\1/p' | tail -1 || true)"
    rm -f "$marker" 2>/dev/null || true
    if [[ -n "$label" ]] && sudo -n true 2>/dev/null; then
      log "Installing Command Line Tools via softwareupdate: $label"
      sudo softwareupdate -i "$label" --verbose || true
      sudo xcode-select --switch /Library/Developer/CommandLineTools >/dev/null 2>&1 || true
      if xcode-select -p >/dev/null 2>&1; then
        log "Xcode Command Line Tools installed: $(xcode-select -p)"
        return 0
      fi
    fi
  fi
  log "Cannot complete the CLT install automatically. Opening Apple's installer path."
  xcode-select --install >/dev/null 2>&1 || true
  block_and_exit "xcode-tools" "Apple's Command Line Tools are required before AI-OS can install Homebrew and restore tools. macOS is not completing the CLI install automatically. Open Software Update, install Command Line Tools for Xcode, then relaunch AI-OS Core Installer." "open 'x-apple.systempreferences:com.apple.Software-Update-Settings.extension'" "Open Software Update and install CLT"
}


install_tools() {
  write_state_running "install-tools"
  # The heaviest phase by far: Xcode command line tools plus the Homebrew casks
  # (1Password, OrbStack, VS Code). The need stays at the policy floor on
  # purpose -- a machine in the 5-10 GiB WARN band is allowed to proceed with a
  # loud warning, and inventing a stricter per-phase number here would silently
  # make this installer refuse Macs that rule 10 says should only be warned.
  disk_gate 0 "developer tools (Xcode command line tools and Homebrew casks)"
  log "Installing/repairing fresh-Mac AI-OS prerequisites"
  install_xcode_clt_if_needed
  install_homebrew_if_needed
  log "Installing baseline CLI tools (per-formula; one flaky formula no longer fails the set)"
  local failed=""
  for formula in git gh node pnpm jq rclone age gnupg mas docker docker-compose tmux hudochenkov/sshpass/sshpass; do
    if brew list --formula "$formula" >/dev/null 2>&1; then
      printf '  OK  %-16s already installed\n' "$formula"
    elif brew install "$formula"; then
      printf '  OK  %-16s installed\n' "$formula"
    else
      printf '  FAIL %-15s\n' "$formula"
      failed="$failed $formula"
    fi
  done
  if [[ -n "$failed" ]]; then
    block_and_exit "brew-install" "These Homebrew formulae failed to install:$failed. Retry installs only what is missing." "$RERUN_CMD" "Retry failed tool installs"
  fi
  for formula in pnpm docker; do
    if brew list --formula "$formula" >/dev/null 2>&1 && ! have "$formula"; then
      brew link --overwrite "$formula" || true
    fi
  done
  # tmux scrollback for long wizard/agent sessions (idempotent append; an
  # existing ~/.tmux.conf that already tunes history-limit is never touched)
  if ! grep -qs 'history-limit' "$HOME/.tmux.conf"; then
    printf 'set -g history-limit 100000\n' >> "$HOME/.tmux.conf"
    log "tmux: set history-limit 100000 in ~/.tmux.conf"
  fi
  if have python3; then
    log "Installing Python user-site automation libraries (openpyxl, pexpect)"
    python3 -m pip install --user openpyxl pexpect \
      || python3 -m pip install --user --break-system-packages openpyxl pexpect \
      || block_and_exit "python-user-packages" "Python automation packages failed to install." "$RERUN_CMD" "Retry Python package install"
  else
    block_and_exit "python3" "python3 is not available after baseline tool install." "$RERUN_CMD" "Retry tool install"
  fi
  log "Installing required desktop apps"
  [[ -d "/Applications/1Password.app" ]] || brew install --cask 1password
  { have op || [[ -x /opt/homebrew/bin/op || -x /usr/local/bin/op ]]; } || brew install --cask 1password-cli
  [[ -d "/Applications/OrbStack.app" ]] || brew install --cask orbstack
  [[ -d "/Applications/Visual Studio Code.app" ]] || brew install --cask visual-studio-code
  log "Opening OrbStack so it can finish first-run Docker setup"
  open -a OrbStack || true
  log "Tool status"
  for tool in brew git gh node pnpm jq rclone age gpg mas op docker sshpass tmux; do
    if have "$tool"; then printf '  OK  %-8s %s\n' "$tool" "$(command -v "$tool")"; else printf '  MISSING %-8s\n' "$tool"; fi
  done
}

# Pure-shell tar safety validation (no python3). Only directories and regular
# files are accepted; links and special files can redirect or escape extraction
# even when every displayed archive pathname looks safe.
validate_tar_listing() {
  local tarball="$1" entry entry_type bad=0 names verbose
  names="$(mktemp "${TMPDIR:-/tmp}/aios-packet-names.XXXXXX")" || return 1
  verbose="$(mktemp "${TMPDIR:-/tmp}/aios-packet-types.XXXXXX")" || { rm -f "$names"; return 1; }
  if ! tar -tzf "$tarball" >"$names" || ! tar -tvzf "$tarball" >"$verbose"; then
    rm -f "$names" "$verbose"
    return 1
  fi
  while IFS= read -r entry; do
    [[ -z "$entry" ]] && continue
    case "$entry" in
      /*|*../*|../*|*/..) printf '%s\n' "$entry" >&2; bad=1 ;;
      "$PACKET_DIR_NAME"|"$PACKET_DIR_NAME"/*) ;;
      *) printf '%s\n' "$entry" >&2; bad=1 ;;
    esac
  done < "$names"
  while IFS= read -r entry; do
    [[ -z "$entry" ]] && continue
    entry_type="${entry:0:1}"
    case "$entry_type" in
      -|d) ;;
      *) printf 'unsafe archive entry type %s\n' "$entry_type" >&2; bad=1 ;;
    esac
  done < "$verbose"
  rm -f "$names" "$verbose"
  return $bad
}

if [[ -n "${_AIOS_INTERNAL_TEST_ARCHIVE:-}" ]]; then
  trap - ERR
  if validate_tar_listing "$_AIOS_INTERNAL_TEST_ARCHIVE"; then exit 0; else exit $?; fi
fi

install_core_packet() {
  write_state_running "packet"
  # Re-measure: free space can collapse during the brew phase that ran before
  # this one. Declared per rule 10 -- target $STATE_DIR/download/packet, one
  # generation (rm -rf then re-fetch), bounded by the published packet size,
  # verified by `shasum -a 256 -c` below before anything is extracted.
  disk_gate 1 "the core restore packet"
  log "Downloading and applying AI-OS Core restore packet"
  local download_dir packet_url checksum_url packet_path checksum_path extract_dir apply_script
  local state_real extract_real packet_root packet_real apply_parent_real
  download_dir="$STATE_DIR/download/packet"
  packet_url="$HOSTED_BASE_URL/aios-core/packets/$PACKET_NAME"
  checksum_url="$packet_url.sha256"
  packet_path="$download_dir/$PACKET_NAME"
  checksum_path="$download_dir/$PACKET_NAME.sha256"
  rm -rf "$download_dir"
  mkdir -p "$download_dir"
  extract_dir="$(mktemp -d "$STATE_DIR/.packet-stage.XXXXXX")" \
    || block_and_exit "packet-extract-root" "Could not create a private packet extraction directory." "$RERUN_CMD" "Retry packet extraction"
  state_real="$(cd "$STATE_DIR" && pwd -P)"
  extract_real="$(cd "$extract_dir" && pwd -P)"
  case "$extract_real" in
    "$state_real"/.packet-stage.*) ;;
    *) block_and_exit "packet-extract-containment" "The packet extraction directory escaped the AI-OS state directory." "$RERUN_CMD" "Retry from a safe state directory" ;;
  esac
  curl -fsSL --proto '=https' --proto-redir '=https' --tlsv1.2 "$packet_url" -o "$packet_path" || block_and_exit "packet-download" "Could not download the AI-OS Core packet from $packet_url." "$RERUN_CMD" "Retry packet download"
  curl -fsSL --proto '=https' --proto-redir '=https' --tlsv1.2 "$checksum_url" -o "$checksum_path" || block_and_exit "packet-checksum-download" "Could not download the AI-OS Core packet checksum from $checksum_url." "$RERUN_CMD" "Retry packet checksum download"
  (cd "$download_dir" && shasum -a 256 -c "$PACKET_NAME.sha256" --status --strict) || block_and_exit "packet-checksum" "Downloaded packet failed checksum verification. The packet and checksum may be out of sync." "rm -rf '$download_dir' && $RERUN_CMD" "Redownload verified packet"
  validate_tar_listing "$packet_path" || block_and_exit "packet-archive-safety" "The packet archive contains an unsafe path. Refusing to extract it." "rm -rf '$download_dir' && $RERUN_CMD" "Redownload safe packet"
  tar --no-same-owner --no-same-permissions -xzf "$packet_path" -C "$extract_dir" \
    || block_and_exit "packet-extract" "The verified packet could not be extracted." "rm -rf '$extract_dir' && $RERUN_CMD" "Retry packet extraction"
  packet_root="$extract_dir/$PACKET_DIR_NAME"
  if [[ ! -d "$packet_root" || -L "$packet_root" ]]; then
    block_and_exit "packet-extract-containment" "The packet root is missing or redirected." "rm -rf '$extract_dir' && $RERUN_CMD" "Redownload packet"
  fi
  packet_real="$(cd "$packet_root" && pwd -P)"
  if [[ "$packet_real" != "$extract_real/$PACKET_DIR_NAME" ]]; then
    block_and_exit "packet-extract-containment" "The extracted packet root escaped its private staging directory." "rm -rf '$extract_dir' && $RERUN_CMD" "Redownload packet"
  fi
  apply_script="$packet_root/scripts/apply.sh"
  if [[ ! -f "$apply_script" || -L "$apply_script" ]]; then
    block_and_exit "packet-apply-missing" "The packet did not contain a regular apply script." "rm -rf '$extract_dir' && $RERUN_CMD" "Redownload packet"
  fi
  apply_parent_real="$(cd "$(dirname "$apply_script")" && pwd -P)"
  if [[ "$apply_parent_real/$(basename "$apply_script")" != "$packet_real/scripts/apply.sh" ]]; then
    block_and_exit "packet-extract-containment" "The packet apply script escaped the verified packet root." "rm -rf '$extract_dir' && $RERUN_CMD" "Redownload packet"
  fi
  [[ -x "$apply_script" ]] || chmod +x "$apply_script" 2>/dev/null || true
  if [[ ! -x "$apply_script" ]]; then
    block_and_exit "packet-apply-missing" "The packet apply script could not be made executable." "rm -rf '$extract_dir' && $RERUN_CMD" "Redownload packet"
  fi
  AIOS_INSTALL_ROOT="$INSTALL_ROOT" AIOS_STATE_DIR="$STATE_DIR" "$apply_script" || block_and_exit "packet-apply" "The packet apply script failed. Review the Terminal output above for the exact failed file/check." "$RERUN_CMD" "Retry packet apply"
  rm -rf "$extract_dir"
  printf '  OK  packet %s\n' "$packet_path"
  printf '  OK  packet receipt %s\n' "$STATE_DIR/receipts/packet-applied.json"
}

install_dashboard_app() {
  write_state_running "dashboard"
  disk_gate 2 "the dashboard app"
  log "Downloading AI-OS Core dashboard application"
  local dashboard_dir="$INSTALL_ROOT/dashboard"
  mkdir -p "$(dirname "$dashboard_dir")"
  # The dashboard repo is PRIVATE: an unauthenticated clone fails with the
  # misleading "Repository not found" (dd02mini W8 field finding). Gate on
  # GitHub auth FIRST with the browser device flow, and route git through gh.
  if ! gh auth status >/dev/null 2>&1; then
    block_and_exit "github-auth" "GitHub sign-in is required to download the AI-OS dashboard (the repository is private). The command below opens github.com in your browser — approve it there, then run the rerun command (or click Check again in the installer)." "gh auth login --web --git-protocol https --hostname github.com" "Sign into GitHub (browser)"
  fi
  gh auth setup-git --hostname github.com >/dev/null 2>&1 || true
  if [[ -d "$dashboard_dir/.git" ]]; then
    git -C "$dashboard_dir" fetch --depth 1 origin main
    git -C "$dashboard_dir" checkout main
    git -C "$dashboard_dir" pull --ff-only origin main
  elif [[ -d "$dashboard_dir" ]] && [[ -n "$(find "$dashboard_dir" -mindepth 1 -maxdepth 1 2>/dev/null | head -1)" ]]; then
    block_and_exit "dashboard-dir-not-empty" "The dashboard target exists but is not a git checkout: $dashboard_dir" "mv '$dashboard_dir' '$dashboard_dir.backup.$(date +%Y%m%d%H%M%S)' && git clone --depth 1 '$DASHBOARD_REPO' '$dashboard_dir' && $RERUN_CMD" "Move existing folder and rerun bootstrap"
  else
    rm -rf "$dashboard_dir"
    git clone --depth 1 "$DASHBOARD_REPO" "$dashboard_dir"
  fi
  if [[ ! -f "$dashboard_dir/package.json" ]]; then
    block_and_exit "dashboard-download" "Dashboard download did not produce package.json at $dashboard_dir." "rm -rf '$dashboard_dir' && git clone --depth 1 '$DASHBOARD_REPO' '$dashboard_dir' && $RERUN_CMD" "Redownload dashboard app"
  fi
  cat > "$INSTALL_ROOT/start-aios-dashboard.sh" <<LAUNCH
#!/usr/bin/env bash
set -euo pipefail
cd "$dashboard_dir"
if ! command -v pnpm >/dev/null 2>&1; then
  echo "pnpm is missing. Re-run: $RERUN_CMD"
  exit 1
fi
if [[ ! -d node_modules ]]; then
  echo "Installing local AI-OS Dashboard dependencies. This may take a few minutes the first time."
  pnpm install --frozen-lockfile
fi
if curl -fsS --max-time 2 http://127.0.0.1:9119/api/health >/dev/null 2>&1; then
  echo "AI-OS Dashboard backend is already running at $LOCAL_DASHBOARD_URL"
  open "$LOCAL_DASHBOARD_URL" >/dev/null 2>&1 || true
  exit 0
fi
echo "Starting local AI-OS Dashboard at $LOCAL_DASHBOARD_URL"
echo "Leave this Terminal window open while using the dashboard. Press Ctrl-C to stop."
AIOS_STANDALONE_ONBOARDING=1 pnpm exec vite --host 127.0.0.1 --port 9119 &
DASHBOARD_PID=\$!
trap 'kill "\$DASHBOARD_PID" >/dev/null 2>&1 || true' EXIT INT TERM
for _ in 1 2 3 4 5 6 7 8 9 10; do
  if curl -fsS --max-time 2 http://127.0.0.1:9119/api/health >/dev/null 2>&1; then
    open "$LOCAL_DASHBOARD_URL" >/dev/null 2>&1 || true
    wait "\$DASHBOARD_PID"
    exit \$?
  fi
  sleep 1
done
echo "Dashboard failed to become healthy on 127.0.0.1:9119." >&2
exit 1
LAUNCH
  chmod +x "$INSTALL_ROOT/start-aios-dashboard.sh"
  printf '  OK  dashboard %s\n' "$dashboard_dir"
  printf '  OK  launcher  %s\n' "$INSTALL_ROOT/start-aios-dashboard.sh"
}

fetch_manifest() {
  local manifest_url="$HOSTED_BASE_URL/aios-core/manifest.json"
  if have curl; then
    curl -fsSL "$manifest_url" -o "$MANIFEST_DEST" \
      || block_and_exit "manifest-download" "Could not fetch the hosted AI-OS Core manifest from $manifest_url." "$RERUN_CMD" "Retry the installer"
  else
    block_and_exit "curl-missing" "curl is required to fetch the hosted AI-OS Core manifest." "xcode-select --install" "Install command line tools"
  fi
}

postflight_gates() {
  write_state_running "postflight"
  log "Postflight gate checks"
  case "${_AIOS_INTERNAL_TEST_OUTCOME:-${AIOS_TEST_OUTCOME:-}}" in
    complete) printf '  PASS: simulated complete outcome.\n'; complete_and_exit ;;
    blocked-xcode) printf '  BLOCKED: simulated Xcode Command Line Tools blocker.\n'; block_and_exit "xcode-tools" "Apple's Command Line Tools are required before AI-OS can install Homebrew and restore tools. macOS is not completing the CLI install automatically. Open Software Update, install Command Line Tools for Xcode, then relaunch AI-OS Core Installer." "open 'x-apple.systempreferences:com.apple.Software-Update-Settings.extension'" "Open Software Update and install CLT" ;;
    blocked-gh) printf '  BLOCKED: simulated GitHub auth blocker.\n'; block_and_exit "github-auth" "GitHub CLI is not signed in. The command below opens github.com in your browser (device flow) — approve it there, then click Check again." "gh auth login --web --git-protocol https --hostname github.com" "Sign into GitHub (browser)" ;;
    blocked-op) printf '  BLOCKED: simulated 1Password auth blocker.\n'; block_and_exit "1password-auth" "1Password CLI is installed, but this Terminal is not signed in." 'eval "$(op signin)"' "Sign into 1Password CLI" ;;
    blocked-unexpected) printf '  Simulating an unanticipated failure for ERR-trap testing.\n'; false ;;
  esac
  if ! test -d "/Applications/1Password.app"; then
    block_and_exit "1password-app" "1Password app is not installed." "brew install --cask 1password 1password-cli" "Install 1Password"
  elif ! have op; then
    block_and_exit "1password-cli" "1Password CLI is not installed." "brew install --cask 1password-cli" "Install 1Password CLI"
  elif ! op account list >/dev/null 2>&1; then
    # Single-quoted literal: $(op signin) must NEVER execute at compose time (audit B3).
    block_and_exit "1password-auth" "1Password CLI is installed, but this Terminal is not signed in." 'eval "$(op signin)"' "Sign into 1Password CLI"
  elif ! have gh; then
    block_and_exit "github-cli" "GitHub CLI is not installed." "brew install gh" "Install GitHub CLI"
  elif ! gh auth status >/dev/null 2>&1; then
    block_and_exit "github-auth" "GitHub CLI is not signed in. The command below opens github.com in your browser (device flow) — approve it there, then click Check again." "gh auth login --web --git-protocol https --hostname github.com" "Sign into GitHub (browser)"
  elif ! have docker; then
    block_and_exit "docker-cli" "Docker CLI is not installed." "brew install docker docker-compose && open -a OrbStack" "Install Docker CLI and open OrbStack"
  elif ! orbstack_docker_ready; then
    block_and_exit "orbstack-runtime" "Docker CLI exists, but the verified OrbStack Docker context is not responding yet." "open -a OrbStack && DOCKER_CONTEXT=orbstack docker info" "Start OrbStack and verify its Docker context"
  elif ! backup_guard_fresh; then
    block_and_exit "backup-freshness" "Ready-to-clone is blocked until backup-guard has produced a green receipt and a restorable hermes-state archive newer than 26 hours." "bash ~/.aios-core/download/bootstrap.sh --backup-guard" "Run backup guard and refresh restore proof"
  else
    printf '  PASS: all required bootstrap gates passed, including backup freshness.\n'
    complete_and_exit
  fi
}

log "AI-OS Core Installer  ·  v${WIZARD_VERSION}  —  latest published version at ${HOSTED_BASE_URL}"
write_state_running "preflight"
fetch_manifest

if [[ -n "${_AIOS_INTERNAL_TEST_OUTCOME:-${AIOS_TEST_OUTCOME:-}}" ]]; then
  postflight_gates
fi


if [[ "$INSTALL_TOOLS" == "1" ]]; then
  install_tools
  install_core_packet
  install_dashboard_app
else
  block_and_exit "install-tools-not-run" "Bootstrap package and manifest are ready, but tool installation/repair has not been run." "$RERUN_CMD" "Run bootstrap install/repair mode"
fi

cat <<'SAFETY'

Safety note:
  Runnable shell commands are printed between bracket lines like this:
  [
  command
  ]
  Copy the command inside the brackets, not the brackets themselves.
  Do not paste any other status text into Terminal.

Retired onboarding sidecars are not part of AI-OS Core. Use the local
dashboard at http://127.0.0.1:9119/onboarding after bootstrap completes.

Privacy guardrail:
  Keep personal archives private; personal restore artifacts are never served from this site.
SAFETY

postflight_gates
