#!/bin/sh
# clawdie-iso operator USB build script
# Produces a bootable FreeBSD XFCE operator USB image with Clawdie-AI pre-bundled.
# All packages are fetched and bundled for fully offline installation.
#
# Runtime detection handles configuration (display, GPU, etc.)
#
# Usage:
#   ./build.sh                          # full build (fetch + assemble)
#   ./build.sh --fetch-only             # fetch packages + memstick only (no root needed)
#   ./build.sh --skip-fetch             # skip ALL fetches — assemble from cache (pkg + memstick)
#   ./build.sh --skip-fetch-pkg         # skip only the package + Clawdie-AI fetch
#   ./build.sh --skip-fetch-memstick    # skip only the FreeBSD memstick fetch
#   ./build.sh --live-default-password  # set live clawdie password to quindecim
#   ./build.sh --clawdie-password ...   # set an explicit live clawdie password
#   ./build.sh --clawdie-version 1.0.2  # pin Clawdie-AI release tag v1.0.2
#   ./build.sh --clawdie-ref main       # bundle a branch/tag/commit ref
#
# Requirements (run on FreeBSD host):
#   pkg install curl         # for fetching
#   pkg install node24 npm-node24  # to bundle clawdie-ai node_modules for offline firstboot
#   pkg install (root)       # for step 5-6 (mdconfig, mount)
#
# The tmp/packages/ directory produced here is dual-purpose:
#   1. Bundled into the ISO for offline installation
#   2. Used to seed zroot/pkg-cache on the installed system for offline jail provisioning

set -e

SCRIPT_DIR="$(dirname "$(realpath "$0")")"
# Make build host command lookup independent from the invoking user's login PATH.
PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:${PATH:-}"
export PATH
NPM_CONFIG_UPDATE_NOTIFIER=false
export NPM_CONFIG_UPDATE_NOTIFIER
TMP_DIR="${SCRIPT_DIR}/tmp"
NPM_CONFIG_GLOBALCONFIG="${TMP_DIR}/npm-globalconfig"
NPM_CONFIG_USERCONFIG="${TMP_DIR}/npm-userconfig"
export NPM_CONFIG_GLOBALCONFIG NPM_CONFIG_USERCONFIG
PKG_LIST_DIR="${SCRIPT_DIR}/packages"
PKG_REPO_DIR="${TMP_DIR}/packages"
NPM_GLOBALS_DIR="${TMP_DIR}/npm-globals"
CACHE_DIR="${TMP_DIR}/cache"
OUTPUT_DIR="${TMP_DIR}/output"
LIVE_SESSION_DIR="${SCRIPT_DIR}/live/operator-session"
mkdir -p "$TMP_DIR"

. "${SCRIPT_DIR}/build.cfg"

BUILD_HOST_USER=""
BUILD_HOST_HOME=""
if [ "$(id -u)" -eq 0 ] && [ -n "${SUDO_USER:-}" ] && [ "${SUDO_USER}" != "root" ]; then
    BUILD_HOST_USER="$SUDO_USER"
    BUILD_HOST_HOME="$(getent passwd "$BUILD_HOST_USER" | cut -d: -f6)"
fi
if [ "$(id -u)" -eq 0 ] && [ -z "${BUILD_HOST_USER}" ]; then
    echo "WARN: build is running as root without SUDO_USER; npm stages cannot drop to a non-root build user."
fi

# --- argument parsing ---
SKIP_PKG_FETCH=0
SKIP_MEMSTICK_FETCH=0
FETCH_ONLY=0
LIVE_DEFAULT_PASSWORD=0
while [ "$#" -gt 0 ]; do
    case "$1" in
        --clawdie-version) CLAWDIE_VERSION="$2"; CLAWDIE_REF="v$2"; shift 2 ;;
        --clawdie-ref)     CLAWDIE_REF="$2"; CLAWDIE_VERSION="$2"; shift 2 ;;
        --skip-fetch)          SKIP_PKG_FETCH=1; SKIP_MEMSTICK_FETCH=1; shift ;;
        --skip-fetch-pkg)      SKIP_PKG_FETCH=1; shift ;;
        --skip-fetch-memstick) SKIP_MEMSTICK_FETCH=1; shift ;;
        --fetch-only)      FETCH_ONLY=1; shift ;;
        --live-default-password) LIVE_DEFAULT_PASSWORD=1; shift ;;
        --assistant-name)  ASSISTANT_NAME="$2"; shift 2 ;;
        --domain)          AGENT_DOMAIN="$2"; shift 2 ;;
        --tz)              TZ="$2"; shift 2 ;;
        --ssh-key)         SSH_PUBLIC_KEY="$2"; shift 2 ;;
        --tailscale-auth-key) TAILSCALE_AUTHKEY="$2"; shift 2 ;;
        --root-password)   ROOT_PASSWORD="$2"; shift 2 ;;
        --clawdie-password) CLAWDIE_USER_PASSWORD="$2"; shift 2 ;;
        *) echo "Unknown arg: $1"; exit 1 ;;
    esac
done

if [ "$LIVE_DEFAULT_PASSWORD" -eq 1 ] && [ -z "${CLAWDIE_USER_PASSWORD:-}" ]; then
    CLAWDIE_USER_PASSWORD="quindecim"
fi

TAILSCALE_AUTH_KEY_BAKED=false
if [ -n "${TAILSCALE_AUTHKEY:-}" ]; then
    TAILSCALE_AUTH_KEY_BAKED=true
fi

LIVE_SSH_PUBKEY_FP=""
if [ -n "${SSH_PUBLIC_KEY:-}" ]; then
    _ssh_fp_tmp=$(mktemp "${TMP_DIR}/live-ssh-pubkey.XXXXXX")
    printf '%s\n' "${SSH_PUBLIC_KEY}" > "${_ssh_fp_tmp}"
    LIVE_SSH_PUBKEY_FP=$(ssh-keygen -lf "${_ssh_fp_tmp}" 2>/dev/null | awk '{print $2}')
    rm -f "${_ssh_fp_tmp}"
    if [ -z "${LIVE_SSH_PUBKEY_FP:-}" ]; then
        echo "ERROR: could not derive fingerprint from --ssh-key input"
        exit 1
    fi
fi

# The release gate runs in the preflight section below — it depends on the
# resolve_* helpers, which are defined later in this script.

# The ISO carries its own product version (ISO_VERSION in build.cfg). It does not
# track any single component. Component versions are resolved below purely for
# provenance in build-manifest.json. A milestone build must declare a version.
if [ -z "${ISO_VERSION:-}" ] || [ "${ISO_VERSION}" = "auto" ]; then
    echo "ERROR: ISO_VERSION must be an explicit product version (e.g. 0.10.0)."
    echo "       The image no longer tracks zot — set ISO_VERSION in build.cfg."
    exit 1
fi
_iso_zot_repo="${ZOT_REPO:-${SCRIPT_DIR}/../zot}"
case "${_iso_zot_repo}" in /*) ;; *) _iso_zot_repo="${SCRIPT_DIR}/${_iso_zot_repo}" ;; esac
ZOT_RESOLVED_VERSION=""
ZOT_RESOLVED_COMMIT=""
if [ -d "${_iso_zot_repo}/.git" ]; then
    ZOT_RESOLVED_VERSION="$(git -C "${_iso_zot_repo}" describe --tags --always 2>/dev/null || true)"
    ZOT_RESOLVED_COMMIT="$(git -C "${_iso_zot_repo}" rev-parse --short=12 HEAD 2>/dev/null || true)"
fi
[ -n "${ZOT_RESOLVED_VERSION}" ] || ZOT_RESOLVED_VERSION="${ZOT_VERSION}"

echo "==> clawdie-iso build"
echo "    ISO     : ${ISO_VERSION}-${BUILD_CHANNEL} (zot ${ZOT_RESOLVED_VERSION})"
echo "    FreeBSD : ${FREEBSD_VERSION} ${FREEBSD_ARCH}"
echo "    Clawdie : ${CLAWDIE_REF}"
echo "    Desktop : ${DEFAULT_DESKTOP}"
echo "    Pkg     : ${DEFAULT_PKG_BRANCH}"
echo "    GPU     : ${GPU_DRIVER:-auto-detect}"
echo "    NVIDIA universal : ${NVIDIA_UNIVERSAL:-NO}"
echo "    Target  : ${TARGET:-baremetal}"
echo "    Colibri : ${FEATURE_COLIBRI:-NO} (agent ${ZOT_VERSION:-} / stage ${COLIBRI_STAGE_AGENT:-YES} / test-agent ${COLIBRI_STAGE_TEST_AGENT:-NO})"
echo ""

# Name the output: clawdie-<freebsd-codename>-<version>.img, where the version is
# the ISO product version (see ISO_VERSION above). Per-build provenance — date,
# clawdie-iso/colibri/zot/clawdie-ai commits — lives in build-manifest.json.
BUILD_FREEBSD_MAJOR="${FREEBSD_RELEASE_SERIES%%.*}"
BUILD_FREEBSD_STAMP="$(printf '%s' "${FREEBSD_RELEASE_SERIES}" | tr 'A-Z' 'a-z')"
case "${BUILD_FREEBSD_MAJOR}" in
    15) BUILD_FREEBSD_CODENAME="quindecim" ;;
    *)  BUILD_FREEBSD_CODENAME="fbsd${BUILD_FREEBSD_STAMP}" ;;
esac
IMAGE_NAME="clawdie-${BUILD_FREEBSD_CODENAME}-${ISO_VERSION}.img"
: > "$NPM_CONFIG_GLOBALCONFIG"
: > "$NPM_CONFIG_USERCONFIG"

# --- helper: read package lists into a single deduplicated list ---
pkg_list_all() {
    # Operator USB: include host, jail, desktop, GPU, and disk-install extras
    # for offline use. Runtime detection handles configuration.
    
    echo "==> Operator USB: including packages (host + jails + XFCE desktop + GPU drivers + disk extras)" >&2
    
    cat \
        "${PKG_LIST_DIR}/pkg-list-host.txt" \
        "${PKG_LIST_DIR}/pkg-list-jails.txt" \
        "${PKG_LIST_DIR}/pkg-list-desktop-base.txt" \
        "${PKG_LIST_DIR}/pkg-list-xfce.txt" \
        "${PKG_LIST_DIR}/pkg-list-xfce-theming.txt" \
        "${PKG_LIST_DIR}/pkg-list-live-operator.txt" \
        "${PKG_LIST_DIR}/pkg-list-disk-install-extras.txt" \
        "${PKG_LIST_DIR}/pkg-list-nvidia-all.txt" \
    | grep -v '^#' | grep -v '^$' | sort -u
}

pkg_list_live_installer() {
    cat \
        "${PKG_LIST_DIR}/pkg-list-desktop-base.txt" \
        "${PKG_LIST_DIR}/pkg-list-xfce.txt" \
        "${PKG_LIST_DIR}/pkg-list-xfce-theming.txt" \
        "${PKG_LIST_DIR}/pkg-list-live-operator.txt" \
    | grep -v '^#' \
    | grep -v '^$' \
    | sort -u
}

pkg_list_live_nvidia() {
    case "${GPU_DRIVER:-}" in
        nvidia-390)
            cat "${PKG_LIST_DIR}/pkg-list-nvidia-390.txt"
            ;;
        nvidia-470)
            cat "${PKG_LIST_DIR}/pkg-list-nvidia-470.txt"
            ;;
        nvidia-590)
            cat "${PKG_LIST_DIR}/pkg-list-nvidia-590.txt"
            ;;
        *)
            return 0
            ;;
    esac | grep -v '^#' | grep -v '^$' | sort -u
}

set_config_line() {
    _file="$1"
    _assignment="$2"
    _name=$(echo "$_assignment" | cut -d= -f1)
    mkdir -p "$(dirname "$_file")"
    touch "$_file"
    if grep -q "^${_name}=" "$_file" 2>/dev/null; then
        sed -i '' "s|^${_name}=.*|${_assignment}|" "$_file"
    else
        echo "$_assignment" >> "$_file"
    fi
}

append_rc_list_values() {
    _file="$1"
    _name="$2"
    shift 2

    mkdir -p "$(dirname "$_file")"
    touch "$_file"

    _current=""
    if grep -q "^${_name}=" "$_file" 2>/dev/null; then
        _current=$(sed -n "s/^${_name}=\"\\(.*\\)\"/\\1/p" "$_file" | head -1)
    fi

    _merged="$_current"
    for _value in "$@"; do
        if [ -z "$_merged" ]; then
            _merged="$_value"
            continue
        fi
        case " ${_merged} " in
            *" ${_value} "*) ;;
            *)
                _merged="${_merged} ${_value}"
                ;;
        esac
    done

    set_config_line "$_file" "${_name}=\"${_merged}\""
}

ensure_fstab_line() {
    _file="$1"
    _pattern="$2"
    _line="$3"
    mkdir -p "$(dirname "$_file")"
    touch "$_file"
    if ! grep -Eq "$_pattern" "$_file" 2>/dev/null; then
        echo "$_line" >> "$_file"
    fi
}

set_fstab_line() {
    _file="$1"
    _pattern="$2"
    _line="$3"
    mkdir -p "$(dirname "$_file")"
    touch "$_file"
    _tmp="${_file}.tmp.$$"
    awk -v pattern="$_pattern" -v line="$_line" '
        BEGIN { replaced = 0 }
        $0 ~ pattern {
            if (!replaced) {
                print line
                replaced = 1
            }
            next
        }
        { print }
        END {
            if (!replaced) {
                print line
            }
        }
    ' "$_file" > "$_tmp"
    mv "$_tmp" "$_file"
}

pkg_archive_for() {
    _pkg_name="$1"
    # Match exact package names only. A loose `${name}-*.pkg` pattern lets a
    # missing package such as `xfce` resolve to `xfce-icons-elementary`, which
    # silently builds an image without the requested desktop session.
    {
        find "${PKG_REPO_DIR}/All" -type f -name "${_pkg_name}-[0-9]*.pkg" 2>/dev/null
        find "${PKG_REPO_DIR}" -maxdepth 2 -type f -name "${_pkg_name}-[0-9]*.pkg" 2>/dev/null
    } | sort -u | tail -1
}

resolve_colibri_paths() {
    _resolved_colibri_repo="${COLIBRI_REPO:-/home/clawdie/ai/colibri}"
    case "${_resolved_colibri_repo}" in
        /*) ;;
        *) _resolved_colibri_repo="${SCRIPT_DIR}/${_resolved_colibri_repo}" ;;
    esac
    if [ -n "${COLIBRI_ARTIFACT_DIR:-}" ]; then
        _resolved_colibri_artifact_dir="${COLIBRI_ARTIFACT_DIR}"
        case "${_resolved_colibri_artifact_dir}" in
            /*) ;;
            *) _resolved_colibri_artifact_dir="${SCRIPT_DIR}/${_resolved_colibri_artifact_dir}" ;;
        esac
    else
        _resolved_colibri_artifact_dir="${_resolved_colibri_repo}/target/release"
    fi
}

resolve_clawdie_ai_repo() {
    _resolved_clawdie_ai_repo="${CLAWDIE_AI_REPO:-/home/clawdie/ai/clawdie-ai}"
    case "${_resolved_clawdie_ai_repo}" in
        /*) ;;
        *) _resolved_clawdie_ai_repo="${SCRIPT_DIR}/${_resolved_clawdie_ai_repo}" ;;
    esac
}

preflight_colibri_artifacts() {
    [ "${FEATURE_COLIBRI:-NO}" = "YES" ] || return 0
    [ "${FETCH_ONLY:-0}" -eq 0 ] || return 0

    resolve_colibri_paths
    _colibri_rc="${_resolved_colibri_repo}/packaging/freebsd/colibri_daemon.in"
    _colibri_newsyslog="${_resolved_colibri_repo}/packaging/freebsd/newsyslog-colibri.conf"
    if [ ! -f "${_colibri_rc}" ]; then
        echo "ERROR: Colibri rc.d source missing: ${_colibri_rc}"
        echo "       Set COLIBRI_REPO=/path/to/colibri or FEATURE_COLIBRI=NO."
        exit 1
    fi
    if ! grep -q '^command="/usr/sbin/daemon"' "${_colibri_rc}"; then
        echo "ERROR: Colibri rc.d source does not supervise with daemon(8): ${_colibri_rc}"
        echo "       Update the Colibri checkout before building; a foreground daemon blocks live boot."
        exit 1
    fi
    if [ ! -f "${_colibri_newsyslog}" ]; then
        echo "ERROR: Colibri newsyslog source missing: ${_colibri_newsyslog}"
        echo "       Set COLIBRI_REPO=/path/to/colibri or FEATURE_COLIBRI=NO."
        exit 1
    fi
    for _colibri_bin in colibri-daemon colibri colibri-mcp; do
        if [ ! -x "${_resolved_colibri_artifact_dir}/${_colibri_bin}" ]; then
            echo "ERROR: Colibri release binary missing: ${_resolved_colibri_artifact_dir}/${_colibri_bin}"
            command -v cargo >/dev/null 2>&1 || \
                echo "       NOTE: rust toolchain not found on this host — install it: pkg install rust"
            echo "       Build first: (cd ${_resolved_colibri_repo} && cargo build --workspace --release)"
            echo "       Or set FEATURE_COLIBRI=NO to skip Colibri staging."
            exit 1
        fi
    done
    if [ "${COLIBRI_STAGE_TEST_AGENT:-NO}" = "YES" ] && \
       [ ! -x "${_resolved_colibri_artifact_dir}/colibri-test-agent" ]; then
        echo "ERROR: Colibri test-agent staging requested, but binary is missing: ${_resolved_colibri_artifact_dir}/colibri-test-agent"
        echo "       Set COLIBRI_STAGE_TEST_AGENT=NO for production images, or rebuild Colibri with cargo build --workspace --release."
        exit 1
    fi
}

resolve_zot_paths() {
    _resolved_zot_repo="${ZOT_REPO:-${SCRIPT_DIR}/../zot}"
    case "${_resolved_zot_repo}" in
        /*) ;;
        *) _resolved_zot_repo="${SCRIPT_DIR}/${_resolved_zot_repo}" ;;
    esac
    if [ -n "${ZOT_ARTIFACT_DIR:-}" ]; then
        _resolved_zot_artifact_dir="${ZOT_ARTIFACT_DIR}"
        case "${_resolved_zot_artifact_dir}" in
            /*) ;;
            *) _resolved_zot_artifact_dir="${SCRIPT_DIR}/${_resolved_zot_artifact_dir}" ;;
        esac
    else
        _resolved_zot_artifact_dir="${_resolved_zot_repo}/bin"
    fi
}

preflight_zot_artifacts() {
    [ "${FEATURE_COLIBRI:-NO}" = "YES" ] || return 0
    [ "${COLIBRI_STAGE_AGENT:-YES}" = "YES" ] || return 0
    [ "${FETCH_ONLY:-0}" -eq 0 ] || return 0

    resolve_zot_paths
    if [ ! -x "${_resolved_zot_artifact_dir}/zot" ]; then
        echo "ERROR: Colibri agent binary missing: ${_resolved_zot_artifact_dir}/zot"
        command -v go >/dev/null 2>&1 || \
            echo "       NOTE: go toolchain not found on this host — install it: pkg install go"
        echo "       The agent has no FreeBSD release — build it first:"
        echo "       (cd ${_resolved_zot_repo} && git checkout ${ZOT_VERSION:-v0.2.47} && \\"
        echo "          ZOT_BUILD_VERSION=\"\${ZOT_VERSION:-v0.2.47}\" && VERSION=\"\${ZOT_BUILD_VERSION#v}\" make build)"
        echo "       Or set COLIBRI_STAGE_AGENT=NO to skip agent staging."
        exit 1
    fi
}

override_networkmgr_package() {
    _networkmgr_pkg=$(find "${PKG_REPO_DIR}/All/Hashed" -maxdepth 1 -type f -name 'networkmgr-*.pkg' | sort | tail -1)
    if [ -z "${_networkmgr_pkg:-}" ]; then
        echo "ERROR: networkmgr package not found under ${PKG_REPO_DIR}/All/Hashed"
        exit 1
    fi

    echo "==> [2c/7] Repacking NetworkMgr for mac_do/mdo policy..."
    _networkmgr_stage=$(mktemp -d "${TMP_DIR}/networkmgr-override.XXXXXX")
    _networkmgr_log="${TMP_DIR}/networkmgr-repack.log"
    if ! node "${SCRIPT_DIR}/scripts/repack-networkmgr-for-mdo.mjs" "${_networkmgr_pkg}" "${_networkmgr_stage}" >"${_networkmgr_log}" 2>&1; then
        cat "${_networkmgr_log}"
        rm -rf "${_networkmgr_stage}"
        echo "ERROR: failed to rebuild local networkmgr package"
        exit 1
    fi

    rm -f "${PKG_REPO_DIR}/All/Hashed"/networkmgr-*.pkg
    mv "${_networkmgr_stage}"/networkmgr-*.pkg "${PKG_REPO_DIR}/All/Hashed/"

    # After replacing NetworkMgr's sudo dependency with the local mac_do/mdo
    # package, remove any fetched sudo archive from the offline repo that will
    # be copied onto the USB image.
    find "${PKG_REPO_DIR}/All" -type f -name 'sudo-*.pkg' -delete

    rm -rf "${_networkmgr_stage}"
}

json_escape() {
    printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g'
}

resolve_clawdie_commit() {
    _ref="$1"
    _repo="https://code.smilepowered.org/clawdie/clawdie-ai.git"
    if printf '%s' "$_ref" | grep -Eq '^[0-9a-fA-F]{40}$'; then
        printf '%s\n' "$_ref"
        return 0
    fi
    if command -v git >/dev/null 2>&1; then
        git ls-remote "$_repo" \
            "refs/heads/${_ref}" \
            "refs/tags/${_ref}^{}" \
            "refs/tags/${_ref}" 2>/dev/null \
            | awk '
                $2 ~ /\^\{\}$/ { print $1; found = 1; exit }
                first == "" { first = $1 }
                END { if (!found && first != "") print first }
            '
    fi
}

resolve_latest_clawdie_tag() {
    _repo_api="https://code.smilepowered.org/api/v1/repos/clawdie/clawdie-ai"
    _repo_git="https://code.smilepowered.org/clawdie/clawdie-ai.git"

    _tag=$(
        curl -fsS "${_repo_api}/releases?limit=20" 2>/dev/null \
            | grep -o '"tag_name":"[^"]*"' \
            | head -1 \
            | cut -d'"' -f4
    )
    if [ -n "$_tag" ]; then
        printf '%s\n' "$_tag"
        return 0
    fi

    git ls-remote --tags "$_repo_git" 2>/dev/null \
        | awk -F/ '$2 == "tags" && $3 ~ /^v[0-9]+\.[0-9]+\.[0-9]+$/ { print $3 }' \
        | sed 's/^v//' \
        | sort -t. -k1,1n -k2,2n -k3,3n \
        | tail -n 1 \
        | sed 's/^/v/'
}

is_pinned_clawdie_ref() {
    _ref="$1"
    printf '%s' "$_ref" | grep -Eq '^[0-9a-fA-F]{40}$|^v[0-9]+\.[0-9]+\.[0-9]+$'
}

# Assert a repo has no uncommitted *or untracked* changes. `git status --porcelain`
# is used (not `git diff`) so stray untracked files — which would change the build
# yet pass a diff-only check — also fail the gate. Increments _release_errors on a
# modified/untracked tree; a non-git path is skipped (nothing to assert).
assert_clean_repo() {
    _acr_name="$1"
    _acr_path="$2"
    command -v git >/dev/null 2>&1 || return 0
    git -C "${_acr_path}" rev-parse --git-dir >/dev/null 2>&1 || return 0
    if [ -n "$(git -C "${_acr_path}" status --porcelain 2>/dev/null)" ]; then
        echo "ERROR: release builds require a clean ${_acr_name} repo (no uncommitted or untracked files)"
        echo "       Modified tree at: ${_acr_path}"
        _release_errors=$((_release_errors + 1))
    fi
}

# Release builds must be reproducible from the recorded provenance. We do not
# require components to be on a named tag — a recorded commit is just as pinned —
# but every staged source must be a clean, committed tree so the manifest's
# commits fully describe the artifact. (build-manifest.json records each commit.)
check_release_gate() {
    _release_errors=0

    assert_clean_repo "clawdie-iso" "${SCRIPT_DIR}"

    resolve_clawdie_ai_repo
    assert_clean_repo "clawdie-ai" "${_resolved_clawdie_ai_repo}"

    if [ "${FEATURE_COLIBRI:-NO}" = "YES" ]; then
        resolve_colibri_paths
        assert_clean_repo "colibri" "${_resolved_colibri_repo}"

        if [ "${COLIBRI_STAGE_AGENT:-YES}" = "YES" ]; then
            resolve_zot_paths
            assert_clean_repo "zot" "${_resolved_zot_repo}"
        fi
    fi

    # Guard against a re-introduced duplicate port. The canonical Colibri FreeBSD
    # port lives in the colibri repo (packaging/freebsd/port/sysutils/colibri) and
    # is consumed from there; a copy in this repo would silently drift from
    # Colibri's Cargo.lock / binaries / license.
    if [ -e "${SCRIPT_DIR}/ports/sysutils/colibri" ]; then
        echo "ERROR: duplicate Colibri port at ports/sysutils/colibri — the canonical port lives in the colibri repo (packaging/freebsd/port/sysutils/colibri); remove this copy."
        _release_errors=$(( _release_errors + 1 ))
    fi

    # A baked mother SSH private key must never reach a publicly hosted release
    # image. Fail fast here so a release build aborts in seconds; the image
    # assembly step also refuses to copy it, as defense in depth.
    if [ -f "/home/clawdie/.ssh/mother-mcp" ]; then
        echo "ERROR: mother SSH key present on build host (/home/clawdie/.ssh/mother-mcp) — refuse to bake it into a release image. Remove it, or build with BUILD_CHANNEL=dev."
        _release_errors=$(( _release_errors + 1 ))
    fi

    if [ "${_release_errors}" -gt 0 ]; then
        echo "ERROR: release build aborted — ${_release_errors} modified repo(s). Use BUILD_CHANNEL=dev for iteration builds."
        exit 1
    fi
}

write_build_manifest() {
    _manifest_path="$1"
    _iso_repo_commit="unknown"
    _iso_repo_modified="null"
    _live_ssh_pubkey_fp_json="null"
    _tailscale_auth_key_baked="${TAILSCALE_AUTH_KEY_BAKED:-false}"
    if command -v git >/dev/null 2>&1 && git -C "$SCRIPT_DIR" rev-parse --git-dir >/dev/null 2>&1; then
        _iso_repo_commit=$(git -C "$SCRIPT_DIR" rev-parse HEAD 2>/dev/null || echo unknown)
        if [ -z "$(git -C "$SCRIPT_DIR" status --porcelain 2>/dev/null)" ]; then
            _iso_repo_modified="false"
        else
            _iso_repo_modified="true"
        fi
    fi
    # Clawdie-AI provenance: the image stages a git checkout of the AI source,
    # so record whether the tree is modified at build time.
    _clawdie_ai_modified="null"
    if command -v git >/dev/null 2>&1; then
        resolve_clawdie_ai_repo
        if git -C "${_resolved_clawdie_ai_repo}" rev-parse --git-dir >/dev/null 2>&1; then
            if [ -z "$(git -C "${_resolved_clawdie_ai_repo}" status --porcelain 2>/dev/null)" ]; then
                _clawdie_ai_modified="false"
            else
                _clawdie_ai_modified="true"
            fi
        fi
    fi
    if [ -n "${LIVE_SSH_PUBKEY_FP:-}" ]; then
        _live_ssh_pubkey_fp_json="\"$(json_escape "${LIVE_SSH_PUBKEY_FP}")\""
    fi
    # Colibri provenance: the image stages adjacent colibri binaries, so record
    # which commit produced them (the biggest gap for a reproducible release).
    _colibri_commit="unknown"
    _colibri_modified="null"
    if [ "${FEATURE_COLIBRI:-NO}" = "YES" ]; then
        resolve_colibri_paths
        if git -C "${_resolved_colibri_repo}" rev-parse --git-dir >/dev/null 2>&1; then
            _colibri_commit=$(git -C "${_resolved_colibri_repo}" rev-parse HEAD 2>/dev/null || echo unknown)
            if [ -z "$(git -C "${_resolved_colibri_repo}" status --porcelain 2>/dev/null)" ]; then
                _colibri_modified="false"
            else
                _colibri_modified="true"
            fi
        fi
    fi
    # Zot provenance (agent binary built from source — record if repo is modified).
    _zot_modified="null"
    if [ "${FEATURE_COLIBRI:-NO}" = "YES" ] && [ "${COLIBRI_STAGE_AGENT:-YES}" = "YES" ]; then
        resolve_zot_paths
        if command -v git >/dev/null 2>&1 && git -C "${_resolved_zot_repo}" rev-parse --git-dir >/dev/null 2>&1; then
            if [ -z "$(git -C "${_resolved_zot_repo}" status --porcelain 2>/dev/null)" ]; then
                _zot_modified="false"
            else
                _zot_modified="true"
            fi
        fi
    fi
    # Pi version provenance. Pi is fetched as @latest (see packages/npm-globals.txt),
    # so the concrete version floats — derive it from the bundled tarball name
    # (earendil-works-pi-coding-agent-<version>.tgz) and record it. This runs after
    # fetch-npm-globals.sh + install_live_npm_globals, so the tarball is present.
    _pi_version="unknown"
    _pi_tgz=$(ls "${NPM_GLOBALS_DIR}"/earendil-works-pi-coding-agent-*.tgz 2>/dev/null | head -1)
    if [ -n "${_pi_tgz}" ]; then
        _pi_version=$(basename "${_pi_tgz}" .tgz | sed 's/^earendil-works-pi-coding-agent-//')
    fi
    mkdir -p "$(dirname "$_manifest_path")"
    cat > "$_manifest_path" <<EOF
{
  "iso_version": "$(json_escape "${ISO_VERSION}")",
  "version_scheme": "product",
  "zot_version": "$(json_escape "${ZOT_RESOLVED_VERSION:-${ZOT_VERSION}}")",
  "zot_commit": "$(json_escape "${ZOT_RESOLVED_COMMIT:-unknown}")",
  "zot_modified": ${_zot_modified:-null},
  "colibri_commit": "$(json_escape "${_colibri_commit:-unknown}")",
  "colibri_modified": ${_colibri_modified:-null},
  "build_channel": "$(json_escape "${BUILD_CHANNEL}")",
  "freebsd_version": "$(json_escape "${FREEBSD_VERSION}")",
  "freebsd_arch": "$(json_escape "${FREEBSD_ARCH}")",
  "clawdie_ai_ref": "$(json_escape "${CLAWDIE_REF}")",
  "clawdie_ai_commit": "$(json_escape "${CLAWDIE_AI_COMMIT:-unknown}")",
  "clawdie_ai_modified": ${_clawdie_ai_modified:-null},
  "pi_version": "$(json_escape "${_pi_version:-unknown}")",
  "live_ssh_pubkey_fp": ${_live_ssh_pubkey_fp_json},
  "tailscale_auth_key_baked": ${_tailscale_auth_key_baked},
  "iso_repo_commit": "$(json_escape "${_iso_repo_commit}")",
  "iso_repo_modified": ${_iso_repo_modified},
  "built_at": "$(date -u '+%Y-%m-%dT%H:%M:%SZ')"
}
EOF
}

mount_memstick_rootfs() {
    _memstick_mount="$1"
    _memstick_slice_img="${CACHE_DIR}/memstick-freebsd-slice.img"
    _memstick_ufs_img="${CACHE_DIR}/memstick-rootfs.img"

    mkdir -p "$_memstick_mount"
    rm -f "$_memstick_slice_img" "$_memstick_ufs_img"

    MD_SRC=$(mdconfig -a -t vnode -f "$MEMSTICK")

    _slice_meta=$(
        gpart show -p "/dev/${MD_SRC}" \
        | awk '$4 == "freebsd" { print $1 " " $2; exit }'
    )
    _slice_start=$(echo "$_slice_meta" | awk '{print $1}')
    _slice_size=$(echo "$_slice_meta" | awk '{print $2}')
    if [ -z "${_slice_start:-}" ] || [ -z "${_slice_size:-}" ]; then
        echo "ERROR: could not determine FreeBSD slice start in ${MEMSTICK}"
        mdconfig -d -u "${MD_SRC}" 2>/dev/null || true
        exit 1
    fi

    truncate -s "$((_slice_size * 512))" "$_memstick_slice_img"
    dd if="$MEMSTICK" of="$_memstick_slice_img" bs=512 skip="$_slice_start" conv=sparse status=none

    MD_SLICE=$(mdconfig -a -t vnode -f "$_memstick_slice_img")
    _ufs_meta=$(
        bsdlabel "/dev/${MD_SLICE}" 2>/dev/null \
        | awk '$1 == "a:" { print $2 " " $3; exit }'
    )
    _ufs_size=$(echo "$_ufs_meta" | awk '{print $1}')
    _ufs_offset=$(echo "$_ufs_meta" | awk '{print $2}')
    if [ -z "${_ufs_size:-}" ] || [ -z "${_ufs_offset:-}" ]; then
        echo "ERROR: could not determine UFS root partition offset in ${MEMSTICK}"
        mdconfig -d -u "${MD_SLICE}" 2>/dev/null || true
        mdconfig -d -u "${MD_SRC}" 2>/dev/null || true
        exit 1
    fi

    truncate -s "$((_ufs_size * 512))" "$_memstick_ufs_img"
    dd if="$_memstick_slice_img" of="$_memstick_ufs_img" bs=512 skip="$_ufs_offset" conv=sparse status=none

    MD_ROOTFS=$(mdconfig -a -t vnode -f "$_memstick_ufs_img")
    mount -r -t ufs "/dev/${MD_ROOTFS}" "$_memstick_mount"
}

cleanup_memstick_rootfs() {
    _memstick_mount="$1"

    umount "$_memstick_mount" 2>/dev/null || true
    [ -n "${MD_ROOTFS:-}" ] && mdconfig -d -u "${MD_ROOTFS}" 2>/dev/null || true
    [ -n "${MD_SLICE:-}" ] && mdconfig -d -u "${MD_SLICE}" 2>/dev/null || true
    [ -n "${MD_SRC:-}" ] && mdconfig -d -u "${MD_SRC}" 2>/dev/null || true
    rm -f "${CACHE_DIR}/memstick-freebsd-slice.img" "${CACHE_DIR}/memstick-rootfs.img"
    MD_ROOTFS=""
    MD_SLICE=""
    MD_SRC=""
}

verify_memstick_cache() {
    if [ ! -f "$MEMSTICK" ] || [ ! -f "${MEMSTICK}.SHA256" ]; then
        return 1
    fi
    _expected_sha=$(
        awk -v image="$(basename "$MEMSTICK")" '
            $2 == "(" image ")" { print $4; exit }
        ' "${MEMSTICK}.SHA256"
    )
    [ -n "${_expected_sha:-}" ] || return 1
    [ "$(/sbin/sha256 -q "$MEMSTICK")" = "$_expected_sha" ]
}

install_image_bootcode() {
    _image_md="$1"
    _image_root="$2"
    _freebsd_slice_index="${3:-}"

    if [ -z "${_freebsd_slice_index}" ]; then
        if [ -e "/dev/${_image_md}s2" ]; then
            _freebsd_slice_index="2"
        else
            _freebsd_slice_index="1"
        fi
    fi

    if [ ! -f "${_image_root}/boot/mbr" ] || [ ! -f "${_image_root}/boot/boot" ]; then
        echo "ERROR: missing bootcode files in ${_image_root}/boot"
        exit 1
    fi

    gpart bootcode -b "${_image_root}/boot/mbr" "/dev/${_image_md}"
    gpart bootcode -b "${_image_root}/boot/boot" "/dev/${_image_md}s${_freebsd_slice_index}"
}

install_image_uefi_bootcode() {
    _image_md="$1"
    _image_root="$2"
    _efi_slice_index="${3:-1}"
    _efi_mount="${CACHE_DIR}/efi-mnt"

    if [ ! -f "${_image_root}/boot/loader.efi" ]; then
        echo "ERROR: missing UEFI loader at ${_image_root}/boot/loader.efi"
        exit 1
    fi

    newfs_msdos -L EFISYS "/dev/${_image_md}s${_efi_slice_index}"
    mkdir -p "${_efi_mount}"
    mount_msdosfs "/dev/${_image_md}s${_efi_slice_index}" "${_efi_mount}"
    mkdir -p "${_efi_mount}/EFI/BOOT"
    cp "${_image_root}/boot/loader.efi" "${_efi_mount}/EFI/BOOT/BOOTX64.EFI"
    sync
    umount "${_efi_mount}"
    rmdir "${_efi_mount}" 2>/dev/null || true
}

run_live_chroot() {
    chroot "$MOUNT_POINT" /usr/bin/env \
        PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" \
        LD_LIBRARY_PATH="/usr/local/lib:/usr/local/lib/compat:/lib:/usr/lib" \
        "$@"
}

refresh_live_desktop_caches() {
    echo "    Refreshing live desktop schema/icon/mime caches..."

    if [ -x "${MOUNT_POINT}/usr/local/bin/glib-compile-schemas" ]; then
        run_live_chroot /usr/local/bin/glib-compile-schemas /usr/local/share/glib-2.0/schemas
    fi
    if [ ! -f "${MOUNT_POINT}/usr/local/share/glib-2.0/schemas/gschemas.compiled" ]; then
        echo "ERROR: GLib GSettings schema cache missing from live image"
        exit 1
    fi

    if [ -x "${MOUNT_POINT}/usr/local/bin/gdk-pixbuf-query-loaders" ]; then
        run_live_chroot /usr/local/bin/gdk-pixbuf-query-loaders --update-cache
    fi
    if [ ! -f "${MOUNT_POINT}/usr/local/lib/gdk-pixbuf-2.0/2.10.0/loaders.cache" ]; then
        echo "ERROR: gdk-pixbuf loader cache missing from live image"
        exit 1
    fi
    if ! grep -qi 'png' "${MOUNT_POINT}/usr/local/lib/gdk-pixbuf-2.0/2.10.0/loaders.cache"; then
        echo "ERROR: gdk-pixbuf PNG loader missing from live image cache"
        exit 1
    fi

    if [ -x "${MOUNT_POINT}/usr/local/bin/update-mime-database" ] && [ -d "${MOUNT_POINT}/usr/local/share/mime" ]; then
        run_live_chroot /usr/local/bin/update-mime-database /usr/local/share/mime
    fi
    if [ -x "${MOUNT_POINT}/usr/local/bin/update-desktop-database" ] && [ -d "${MOUNT_POINT}/usr/local/share/applications" ]; then
        run_live_chroot /usr/local/bin/update-desktop-database /usr/local/share/applications
    fi
    if [ -x "${MOUNT_POINT}/usr/local/bin/gtk-update-icon-cache" ]; then
        # Regenerate every installed icon theme cache. FreeBSD packages and
        # Clawdie branding install icons into hicolor and other theme
        # directories; missed caches show up as blank/dot panel icons.
        for _icon_theme_dir in "${MOUNT_POINT}/usr/local/share/icons"/*/; do
            [ -d "$_icon_theme_dir" ] || continue
            _theme_name=$(basename "$_icon_theme_dir")
            run_live_chroot /usr/local/bin/gtk-update-icon-cache -f -t "/usr/local/share/icons/${_theme_name}" || true
        done
    fi

    if [ -x "${MOUNT_POINT}/usr/local/bin/fc-cache" ] && [ -d "${MOUNT_POINT}/usr/local/share/fonts" ]; then
        run_live_chroot /usr/local/bin/fc-cache -f
    fi
}

ensure_sshd_include_line() {
    _sshd_config="$1"
    _include_line='Include /etc/ssh/sshd_config.d/*.conf'

    if [ ! -f "$_sshd_config" ]; then
        echo "ERROR: sshd_config missing from live image"
        exit 1
    fi

    if grep -Eq '^[[:space:]]*Include[[:space:]]+/etc/ssh/sshd_config\.d/\*\.conf([[:space:]]|$)' "$_sshd_config"; then
        return 0
    fi

    _tmp="${_sshd_config}.tmp.$$"
    {
        printf '%s\n' "${_include_line}"
        cat "$_sshd_config"
    } > "$_tmp"
    mv "$_tmp" "$_sshd_config"
}

configure_nsswitch_hosts_line() {
    _file="$1"
    _line='hosts: files mdns_minimal [NOTFOUND=return] dns mdns'
    if [ ! -f "$_file" ]; then
        echo "ERROR: nsswitch.conf missing from live image"
        exit 1
    fi
    set_fstab_line "$_file" '^[[:space:]]*hosts:' "$_line"
}

install_live_runtime_packages() {
    echo "    Installing live operator runtime packages into image..."

    _pkg_files=""
    for _pkg in $(pkg_list_live_installer); do
        _archive=$(pkg_archive_for "$_pkg")
        if [ -z "${_archive:-}" ]; then
            echo "ERROR: missing package archive for ${_pkg}"
            exit 1
        fi
        _pkg_files="${_pkg_files} ${_archive}"
    done
    for _pkg in $(pkg_list_live_nvidia); do
        _archive=$(pkg_archive_for "$_pkg")
        if [ -z "${_archive:-}" ]; then
            echo "ERROR: missing NVIDIA package archive for ${_pkg}"
            exit 1
        fi
        _pkg_files="${_pkg_files} ${_archive}"
    done

    mkdir -p "${MOUNT_POINT}/dev" "${MOUNT_POINT}/proc"
    _mounted_devfs=0
    _mounted_procfs=0

    if mount -t devfs devfs "${MOUNT_POINT}/dev"; then
        _mounted_devfs=1
    fi
    if mount -t procfs proc "${MOUNT_POINT}/proc"; then
        _mounted_procfs=1
    fi

    if ! env ASSUME_ALWAYS_YES=yes HANDLE_RC_SCRIPTS=no \
        /usr/local/sbin/pkg-static -o PKG_TRIGGERS_ENABLE=false -r "$MOUNT_POINT" add -f ${_pkg_files}; then
        [ "$_mounted_procfs" -eq 1 ] && umount "${MOUNT_POINT}/proc" 2>/dev/null || true
        [ "$_mounted_devfs" -eq 1 ] && umount "${MOUNT_POINT}/dev" 2>/dev/null || true
        echo "ERROR: failed to install live GUI runtime packages into image"
        exit 1
    fi

    if ! chroot "${MOUNT_POINT}" /usr/sbin/pw usershow sddm >/dev/null 2>&1; then
        [ "$_mounted_procfs" -eq 1 ] && umount "${MOUNT_POINT}/proc" 2>/dev/null || true
        [ "$_mounted_devfs" -eq 1 ] && umount "${MOUNT_POINT}/dev" 2>/dev/null || true
        echo "ERROR: sddm user missing from live image after package install"
        exit 1
    fi

    # webcamd creates /dev/videoN with mode 0660 root:webcamd. Add clawdie
    # to that group so the operator can open the camera without sudo.
    if chroot "${MOUNT_POINT}" /usr/sbin/pw groupshow webcamd >/dev/null 2>&1; then
        chroot "${MOUNT_POINT}" /usr/sbin/pw groupmod webcamd -m clawdie 2>/dev/null || true
    else
        [ "$_mounted_procfs" -eq 1 ] && umount "${MOUNT_POINT}/proc" 2>/dev/null || true
        [ "$_mounted_devfs" -eq 1 ] && umount "${MOUNT_POINT}/dev" 2>/dev/null || true
        echo "ERROR: webcamd group missing from live image after package install"
        exit 1
    fi

    # Lock the repacked networkmgr package so pkg upgrade doesn't replace it
    # with the upstream version that depends on sudo. The ISO uses mdo/mac_do
    # instead of sudo; see override_networkmgr_package() above.
    if ! chroot "${MOUNT_POINT}" /usr/local/sbin/pkg lock -y networkmgr; then
        [ "$_mounted_procfs" -eq 1 ] && umount "${MOUNT_POINT}/proc" 2>/dev/null || true
        [ "$_mounted_devfs" -eq 1 ] && umount "${MOUNT_POINT}/dev" 2>/dev/null || true
        echo "ERROR: failed to lock networkmgr package on live image"
        exit 1
    fi

    refresh_live_desktop_caches

    [ "$_mounted_procfs" -eq 1 ] && umount "${MOUNT_POINT}/proc" 2>/dev/null || true
    [ "$_mounted_devfs" -eq 1 ] && umount "${MOUNT_POINT}/dev" 2>/dev/null || true
}

install_firefox_extensions() {
    # Pre-stage uBlock Origin and Bitwarden, and apply enterprise policies
    # (no default-browser check — Firefox is the only browser on the USB).
    local _ff_dist="${MOUNT_POINT}/usr/local/lib/firefox/distribution"
    local _ext_dir="${_ff_dist}/extensions"
    local _policies="${_ff_dist}/policies.json"

    echo "    Configuring Firefox..."

    mkdir -p "${_ext_dir}" || {
        echo "ERROR: failed to create Firefox distribution directory"
        exit 1
    }

    # Disable "make default browser" popup — Firefox is the only browser.
    cat > "${_policies}" <<'POLICIES'
{
  "policies": {
    "DontCheckDefaultBrowser": true
  }
}
POLICIES
    chmod 0644 "${_policies}"

    # Download a Firefox extension XPI if not already cached.
    # Usage: _fetch_xpi <slug> <extension-id> <label>
    _fetch_xpi() {
        local _slug="$1" _id="$2" _label="$3"
        local _xpi="${_ext_dir}/${_id}.xpi"
        local _url="https://addons.mozilla.org/firefox/downloads/latest/${_slug}/latest.xpi"

        if [ -f "${_xpi}" ]; then
            echo "    ${_label} XPI already cached, skipping download"
            return 0
        fi

        echo "    Downloading ${_label}..."
        /usr/bin/fetch -o "${_xpi}" "${_url}" || {
            echo "ERROR: failed to download ${_label} XPI"
            exit 1
        }
    }

    _fetch_xpi "ublock-origin" "uBlock0@raymondhill.net" "uBlock Origin"
    _fetch_xpi "sponsorblock" "sponsorBlocker@ajay.app" "SponsorBlock"
    _fetch_xpi "istilldontcareaboutcookies" "idcac-pub@guus.ninja" "I still don't care about cookies"
    _fetch_xpi "bitwarden-password-manager" "{446900e4-71c2-419f-a6a7-df9c091e268b}" "Bitwarden"

    chmod 0755 "${_ext_dir}"
    echo "    Firefox extensions staged — auto-installed on first launch"
}

install_colibri_service() {
    [ "${FEATURE_COLIBRI:-NO}" = "YES" ] || {
        echo "    Colibri service staging disabled (FEATURE_COLIBRI=${FEATURE_COLIBRI:-NO})"
        return 0
    }

    echo "    Staging Colibri service..."

    resolve_colibri_paths

    env \
        COLIBRI_REPO="${_resolved_colibri_repo}" \
        COLIBRI_ARTIFACT_DIR="${_resolved_colibri_artifact_dir}" \
        COLIBRI_STAGE_ENABLE="${COLIBRI_DAEMON_ENABLE:-YES}" \
        COLIBRI_STAGE_TEST_AGENT="${COLIBRI_STAGE_TEST_AGENT:-NO}" \
        COLIBRI_COST_MODE="${COLIBRI_COST_MODE:-smart}" \
        "${SCRIPT_DIR}/scripts/stage-colibri-iso.sh" "${MOUNT_POINT}"

    if ! /usr/sbin/pw -R "${MOUNT_POINT}" groupshow colibri >/dev/null 2>&1; then
        /usr/sbin/pw -R "${MOUNT_POINT}" groupadd colibri
    fi
    if ! /usr/sbin/pw -R "${MOUNT_POINT}" usershow colibri >/dev/null 2>&1; then
        /usr/sbin/pw -R "${MOUNT_POINT}" useradd colibri \
            -g colibri \
            -d /var/db/colibri \
            -s /usr/sbin/nologin \
            -c "Colibri Daemon"
    fi

    mkdir -p \
        "${MOUNT_POINT}/var/db/colibri" \
        "${MOUNT_POINT}/var/run/colibri" \
        "${MOUNT_POINT}/var/log/colibri"
    chroot "${MOUNT_POINT}" chown -R colibri:colibri \
        /var/db/colibri \
        /var/run/colibri \
        /var/log/colibri
    # 0750 matches the rc.d prestart (install -d -m 0750); the daemon dirs hold
    # the SQLite DB and logs and should not be world-readable. The operator
    # reaches them via the colibri group, not "other".
    chmod 0750 \
        "${MOUNT_POINT}/var/db/colibri" \
        "${MOUNT_POINT}/var/run/colibri" \
        "${MOUNT_POINT}/var/log/colibri"

    # Allow operator to use 'colibri' CLI without root
    if /usr/sbin/pw -R "${MOUNT_POINT}" usershow clawdie >/dev/null 2>&1; then
        /usr/sbin/pw -R "${MOUNT_POINT}" groupmod colibri -m clawdie
    fi

    set_config_line "${MOUNT_POINT}/etc/rc.conf" "colibri_daemon_enable=\"${COLIBRI_DAEMON_ENABLE:-YES}\""
    # Operator-image only: require the first-boot password gate to have run
    # (it writes /var/db/colibri/.secured) before the daemon autospawns an
    # agent. Opt-in so deployed colibri hosts (no firstboot gate) are unaffected.
    # Consumed by colibri_daemon.in prestart (colibri repo).
    set_config_line "${MOUNT_POINT}/etc/rc.conf" 'colibri_daemon_require_secured="YES"'
    set_config_line "${MOUNT_POINT}/etc/rc.conf" 'colibri_daemon_user="colibri"'
    set_config_line "${MOUNT_POINT}/etc/rc.conf" 'colibri_daemon_group="colibri"'
    set_config_line "${MOUNT_POINT}/etc/rc.conf" 'colibri_daemon_data_dir="/var/db/colibri"'
    set_config_line "${MOUNT_POINT}/etc/rc.conf" 'colibri_daemon_run_dir="/var/run/colibri"'
    set_config_line "${MOUNT_POINT}/etc/rc.conf" 'colibri_daemon_socket="/var/run/colibri/colibri.sock"'
    set_config_line "${MOUNT_POINT}/etc/rc.conf" 'colibri_daemon_db_path="/var/db/colibri/colibri.sqlite"'
    set_config_line "${MOUNT_POINT}/etc/rc.conf" 'colibri_daemon_logfile="/var/log/colibri/daemon.log"'
    set_config_line "${MOUNT_POINT}/etc/rc.conf" 'colibri_daemon_provider_env="/usr/local/etc/colibri/provider.env"'
    set_config_line "${MOUNT_POINT}/etc/rc.conf" 'colibri_daemon_host="$(/bin/hostname)"'
    set_config_line "${MOUNT_POINT}/etc/rc.conf" "colibri_daemon_cost_mode=\"${COLIBRI_COST_MODE:-smart}\""

    if [ ! -x "${MOUNT_POINT}/usr/local/bin/colibri-daemon" ] || \
       [ ! -x "${MOUNT_POINT}/usr/local/bin/colibri" ] || \
       [ ! -x "${MOUNT_POINT}/usr/local/bin/colibri-mcp" ]; then
        echo "ERROR: Colibri binaries missing from live image"
        exit 1
    fi
    if [ ! -x "${MOUNT_POINT}/usr/local/etc/rc.d/colibri_daemon" ]; then
        echo "ERROR: Colibri rc.d script missing from live image"
        exit 1
    fi
    if ! /usr/sbin/pw -R "${MOUNT_POINT}" usershow colibri >/dev/null 2>&1; then
        echo "ERROR: colibri service user missing from live image"
        exit 1
    fi

    # Operator helper: one-command live rebuild/redeploy of Colibri from source
    # (automates docs/LIVE-COLIBRI-REBUILD.md). Only staged with the service.
    install -m 0755 "${LIVE_SESSION_DIR}/colibri-live-rebuild" \
        "${MOUNT_POINT}/usr/local/bin/colibri-live-rebuild"

    # Seed the skills catalog with operator-useful entries. The daemon
    # creates the SQLite DB on first start; pre-populate it so skills
    # are available immediately without a first-boot script.
    _colibri_db="${MOUNT_POINT}/var/db/colibri/colibri.sqlite"
    if command -v sqlite3 >/dev/null 2>&1; then
        sqlite3 "${_colibri_db}" "CREATE TABLE IF NOT EXISTS skills (
            id TEXT PRIMARY KEY NOT NULL,
            name TEXT NOT NULL UNIQUE,
            description TEXT,
            category TEXT,
            created_at TEXT NOT NULL
        );" 2>/dev/null || true
        _now=$(date -u +%Y-%m-%dT%H:%M:%SZ)
        sqlite3 "${_colibri_db}" "INSERT OR IGNORE INTO skills (id, name, description, category, created_at) VALUES
            ('$(uuidgen || echo 00000000-0000-0000-0000-000000000001)', 'freebsd-live-usb', 'FreeBSD live USB operator workstation procedures', 'freebsd', '${_now}'),
            ('$(uuidgen || echo 00000000-0000-0000-0000-000000000002)', 'colibri-test', 'Colibri daemon startup check and validation', 'colibri', '${_now}'),
            ('$(uuidgen || echo 00000000-0000-0000-0000-000000000003)', 'iso-build', 'Clawdie ISO build and staging workflow', 'iso', '${_now}'),
            ('$(uuidgen || echo 00000000-0000-0000-0000-000000000004)', 'tailscale-join', 'Tailscale mesh join procedure for operator USB', 'networking', '${_now}'),
            ('$(uuidgen || echo 00000000-0000-0000-0000-000000000005)', 'disk-deploy', 'Deploy from USB live to permanent disk install. Provisions ZFS pool, installs FreeBSD boot environment, migrates config, and prepares for the future deployed-system clawdie service.', 'clawdie', '${_now}'),
            ('$(uuidgen || echo 00000000-0000-0000-0000-000000000006)', 'deployed-clawdie-health', 'Future post-deploy health check for service clawdie once the deployed-system service implementation lands.', 'clawdie', '${_now}');" 2>/dev/null || true
        chroot "${MOUNT_POINT}" chown colibri:colibri /var/db/colibri/colibri.sqlite 2>/dev/null || true
        echo "      colibri skills seeded: 6 entries"
    fi

    # Import clawdie-ai skill definitions into the catalog.
    # Reads .agent/skills/*/SKILL.md and registers name + description.
    resolve_clawdie_ai_repo
    _clawdie_ai_dir="${_resolved_clawdie_ai_repo}"
    if [ -d "${_clawdie_ai_dir}/.agent/skills" ]; then
        "${SCRIPT_DIR}/scripts/import-clawdie-skills.sh" \
            "${_clawdie_ai_dir}" "${MOUNT_POINT}"
    else
        echo "    clawdie-ai checkout not found, skipping skill import"
    fi
}

install_zot_agent() {
    [ "${FEATURE_COLIBRI:-NO}" = "YES" ] || {
        echo "    Colibri agent staging disabled (FEATURE_COLIBRI=${FEATURE_COLIBRI:-NO})"
        return 0
    }
    [ "${COLIBRI_STAGE_AGENT:-YES}" = "YES" ] || {
        echo "    Colibri agent staging disabled (COLIBRI_STAGE_AGENT=${COLIBRI_STAGE_AGENT:-YES})"
        return 0
    }

    echo "    Staging Colibri agent (${ZOT_VERSION:-})..."
    resolve_zot_paths

    env \
        ZOT_ARTIFACT_DIR="${_resolved_zot_artifact_dir}" \
        ZOT_OPERATOR="clawdie" \
        ZOT_DEEPSEEK_KEY="${ZOT_DEEPSEEK_KEY:-}" \
        "${SCRIPT_DIR}/scripts/stage-zot-iso.sh" "${MOUNT_POINT}"

    # zot state lives in the operator's home; own it as the operator.
    if /usr/sbin/pw -R "${MOUNT_POINT}" usershow clawdie >/dev/null 2>&1; then
        chroot "${MOUNT_POINT}" chown -R clawdie:clawdie /home/clawdie/.local 2>/dev/null || true
    fi

    if [ ! -x "${MOUNT_POINT}/usr/local/bin/zot" ]; then
        echo "ERROR: zot binary missing from live image"
        exit 1
    fi
}

# Stage an on-image NVIDIA pkg repo (all branches) so clawdie_live_gpu can
# `pkg install` the detected branch at boot (NVIDIA_UNIVERSAL lane).
#
# FreeBSD-build-host step (authored on Linux; runs + must be validated on
# FreeBSD). Verify on the build host: (1) the `pkg fetch -o` layout matches what
# `pkg repo` expects, (2) the dependency closure is complete for offline boot
# Build the Astro docs site and stage it as static HTML on the ISO.
# When ISO_VERSION is set, the docs carry a version badge matching the image.
# Skips silently if node/npm are unavailable.
build_and_stage_docs() {
    local _docs_src="${SCRIPT_DIR}/docs/website"
    local _docs_out="${MOUNT_POINT}/usr/local/share/clawdie-iso/docs"

    if [ ! -d "${_docs_src}" ]; then
        echo "    Docs source not found at ${_docs_src} — skipping"
        return 0
    fi
    if ! command -v node >/dev/null 2>&1 || ! command -v npm >/dev/null 2>&1; then
        echo "    Node/npm not available — skipping docs build"
        return 0
    fi

    echo "    Building docs (ISO ${ISO_VERSION})..."
    (
        cd "${_docs_src}"
        npm ci --silent 2>&1 || { echo "    WARN: npm ci failed, skipping docs"; exit 0; }
        ASTRO_ISO_VERSION="${ISO_VERSION}" \
        ASTRO_SITE_URL="file:///usr/local/share/clawdie-iso/docs" \
            npm run build --silent 2>&1 || { echo "    WARN: astro build failed, skipping docs"; exit 0; }
    )

    if [ -d "${_docs_src}/dist" ]; then
        mkdir -p "${_docs_out}"
        cp -a "${_docs_src}/dist/." "${_docs_out}/"
        echo "    Docs staged at /usr/local/share/clawdie-iso/docs/ (version ${ISO_VERSION})"
    fi
}

# FreeBSD-build-host step (authored on Linux; runs + must be validated on
# FreeBSD). Verify on the build host: (1) the `pkg fetch -o` layout matches what
# `pkg repo` expects, (2) the dependency closure is complete for offline boot
# install, (3) image size headroom, (4) `pkg install -r clawdie-nvidia` resolves
# from file:// at boot. See doc handoff.
install_nvidia_universal_repo() {
    [ "${NVIDIA_UNIVERSAL:-NO}" = "YES" ] || {
        echo "    NVIDIA universal repo staging disabled (NVIDIA_UNIVERSAL=${NVIDIA_UNIVERSAL:-NO})"
        return 0
    }

    echo "    Staging universal NVIDIA repo (all branches) into image..."
    _nv_repo="${MOUNT_POINT}/usr/local/share/clawdie/nvidia-repo"
    mkdir -p "${_nv_repo}"

    # Fetch all three branches + deps into the on-image repo dir, then build repo
    # metadata. Uses pkg's own dependency resolution so the closure is complete.
    if ! env ASSUME_ALWAYS_YES=yes pkg fetch -y -o "${_nv_repo}" -d \
        nvidia-driver-390 nvidia-driver-470 nvidia-driver-580 nvidia-settings nvidia-xconfig; then
        echo "ERROR: failed to fetch NVIDIA packages for universal repo"
        exit 1
    fi
    if ! pkg repo "${_nv_repo}"; then
        echo "ERROR: failed to generate NVIDIA universal repo metadata"
        exit 1
    fi

    mkdir -p "${MOUNT_POINT}/usr/local/etc/pkg/repos"
    cat > "${MOUNT_POINT}/usr/local/etc/pkg/repos/clawdie-nvidia.conf" <<'EOF'
# On-image NVIDIA package repo for the universal GPU lane. Consumed at boot by
# clawdie_live_gpu (nvidia-auto mode) via: pkg install -r clawdie-nvidia ...
clawdie-nvidia: {
    url: "file:///usr/local/share/clawdie/nvidia-repo",
    enabled: yes
}
EOF
    echo "    NVIDIA universal repo staged at /usr/local/share/clawdie/nvidia-repo"
}

install_live_npm_globals() {
    echo "    Installing bundled npm globals into live image..."

    if [ ! -d "$NPM_GLOBALS_DIR" ]; then
        echo "    WARN: ${NPM_GLOBALS_DIR} not found — skipping live npm globals"
        return 0
    fi

    _live_prefix="${MOUNT_POINT}/opt/clawdie/npm-global"
    mkdir -p "${_live_prefix}/bin" "${_live_prefix}/lib" "${MOUNT_POINT}/usr/local/bin"

    _live_npm_home="${CACHE_DIR}/npm-live-home"
    rm -rf "${_live_npm_home}"
    mkdir -p "${_live_npm_home}"

    for _tgz in "${NPM_GLOBALS_DIR}"/*.tgz; do
        [ -f "${_tgz}" ] || continue
        # Live bundle policy: ship pi only via npm tarball. codex is covered
        # separately via the FreeBSD pkg in pkg-list-live-operator.txt.
        case "$(basename "${_tgz}")" in
            *claude-code*)
                echo "      skip $(basename "${_tgz}") (broken on FreeBSD live image)"
                continue
                ;;
            *gemini-cli*)
                echo "      skip $(basename "${_tgz}") (not part of current live CLI policy)"
                continue
                ;;
        esac
        echo "      npm install -g $(basename "${_tgz}")"
        env HOME="${_live_npm_home}" npm_config_prefix="${_live_prefix}" npm install -g --ignore-scripts --no-audit --no-fund "${_tgz}" >/dev/null
    done

    for _bin in "${_live_prefix}/bin"/*; do
        [ -e "${_bin}" ] || continue
        ln -sf "/opt/clawdie/npm-global/bin/$(basename "${_bin}")" "${MOUNT_POINT}/usr/local/bin/$(basename "${_bin}")"
    done

    patch_live_pi_footer_hostname "${_live_prefix}"

    # npm runs as root on the build host and writes everything as
    # root:wheel. configure_live_operator_session() used to do this
    # chown but ran BEFORE install_live_npm_globals (caller order at
    # bottom of build.sh), so it was chowning two empty directories
    # before the package tree was written. Do it here, after the files
    # exist, then validate — fail the build if ownership didn't take
    # so this regression cannot ship silently again.
    if ! chroot "${MOUNT_POINT}" /usr/sbin/pw usershow clawdie >/dev/null 2>&1; then
        echo "ERROR: clawdie user missing — cannot chown npm-global tree"
        exit 1
    fi
    chroot "${MOUNT_POINT}" chown -R clawdie:clawdie /opt/clawdie
    _npm_owner=$(chroot "${MOUNT_POINT}" /usr/bin/stat -f '%Su:%Sg' /opt/clawdie/npm-global)
    if [ "${_npm_owner}" != "clawdie:clawdie" ]; then
        echo "ERROR: /opt/clawdie/npm-global ownership is ${_npm_owner}, expected clawdie:clawdie"
        exit 1
    fi

    # Fix pi HTTP/2: the bundled http-dispatcher sets allowH2:false to
    # work around a Node 26.0 undici decompression bug. We run Node 24
    # with undici 8.3, where H2 works correctly. Enabling HTTP/2 gives
    # HPACK header compression and multiplexing for API calls.
    _pi_dispatcher="${_live_prefix}/lib/node_modules/@earendil-works/pi-coding-agent/dist/core/http-dispatcher.js"
    if [ -f "${_pi_dispatcher}" ]; then
        sed -i '' 's/allowH2: false/allowH2: true/' "${_pi_dispatcher}"
        echo "      pi http-dispatcher: allowH2 → true"
    fi
}

patch_live_pi_footer_hostname() {
    _npm_prefix="$1"
    _patch_script="${SCRIPT_DIR}/firstboot/patch-pi-footer-hostname.js"
    _patched=0

    if [ ! -f "${_patch_script}" ]; then
        echo "ERROR: Pi footer patch script missing: ${_patch_script}"
        exit 1
    fi

    for _footer in \
        "${_npm_prefix}/lib/node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/components/footer.js" \
        "${_npm_prefix}/lib/node_modules/@mariozechner/pi-coding-agent/dist/modes/interactive/components/footer.js"
    do
        [ -f "${_footer}" ] || continue
        echo "      patch Pi footer hostname: ${_footer}"
        node "${_patch_script}" "${_footer}"
        node --check "${_footer}"
        _patched=1
    done

    if [ "${_patched}" -ne 1 ]; then
        echo "ERROR: Pi footer.js not found under ${_npm_prefix}"
        exit 1
    fi
}

seed_live_ai_source_repo() {
    _repo_src="$1"
    _repo_name="$2"
    _repo_dest="${MOUNT_POINT}/home/clawdie/ai/${_repo_name}"

    if [ ! -d "${_repo_src}" ]; then
        echo "    Skipping AI source seed ${_repo_name}: ${_repo_src} not found"
        return 0
    fi
    if ! command -v git >/dev/null 2>&1 || ! git -C "${_repo_src}" rev-parse --git-dir >/dev/null 2>&1; then
        echo "    Skipping AI source seed ${_repo_name}: not a git worktree"
        return 0
    fi

    _repo_branch=$(git -C "${_repo_src}" symbolic-ref --short -q HEAD 2>/dev/null || echo detached)
    _repo_commit=$(git -C "${_repo_src}" rev-parse HEAD 2>/dev/null || echo unknown)
    _repo_origin=$(git -C "${_repo_src}" remote get-url origin 2>/dev/null || echo unknown)
    _repo_src_real=$(cd "${_repo_src}" && pwd -P)

    echo "    Seeding AI source checkout: ${_repo_name} (${_repo_commit})"
    rm -rf "${_repo_dest}"
    if [ "${_repo_branch}" != "detached" ]; then
        git clone --quiet --depth 1 --branch "${_repo_branch}" "file://${_repo_src_real}" "${_repo_dest}"
    else
        git clone --quiet "file://${_repo_src_real}" "${_repo_dest}"
        git -C "${_repo_dest}" checkout --quiet --detach "${_repo_commit}"
    fi
    git -C "${_repo_dest}" remote set-url origin "${_repo_origin}" 2>/dev/null || true
    printf '%s\n' '.clawdie-source.json' >> "${_repo_dest}/.git/info/exclude"

    _repo_modified=false
    if [ -n "$(git -C "${_repo_src}" status --porcelain 2>/dev/null)" ]; then
        _repo_modified=true
    fi

    cat > "${_repo_dest}/.clawdie-source.json" <<EOF
{
  "name": "$(json_escape "${_repo_name}")",
  "source_path": "$(json_escape "${_repo_src}")",
  "origin": "$(json_escape "${_repo_origin}")",
  "branch": "$(json_escape "${_repo_branch}")",
  "commit": "$(json_escape "${_repo_commit}")",
  "modified_at_build": ${_repo_modified},
  "iso_version": "$(json_escape "${ISO_VERSION}")",
  "build_channel": "$(json_escape "${BUILD_CHANNEL}")",
  "snapshot_kind": "shallow git checkout",
  "snapshot_note": "shallow git checkout of HEAD; uncommitted changes and ignored/private files are not included"
}
EOF
}

install_live_ai_source_snapshots() {
    echo "    Installing live AI source checkouts..."
    resolve_colibri_paths
    mkdir -p "${MOUNT_POINT}/home/clawdie/ai"
    cat > "${MOUNT_POINT}/home/clawdie/ai/README.txt" <<'EOF'
Clawdie live AI source checkouts

These directories are included so the operator can inspect the shipped source
beside the running system and fetch follow-up commits from Forgejo when network
access is available.

No API keys, .env files, SSH private keys, build caches, package caches, tmp/
directories, or uncommitted worktree changes are included. Each checkout keeps a
.git directory plus a .clawdie-source.json file recording the source remote,
branch, commit, modified state, ISO version, and build channel at image build time.

clawdie-ai (TypeScript) is no longer included — it is being phased out in favor
of the colibri (Rust) control plane. The `clawdie` name is retained as the brand
and bare-metal installer identity.

hermes-bsd (Python) is the agent CLI and gateway — powers the Telegram bot,
agent-to-agent relay, and the operator's primary chat interface.
EOF
    seed_live_ai_source_repo "${SCRIPT_DIR}" "clawdie-iso"
    seed_live_ai_source_repo "${_resolved_colibri_repo}" "colibri"
    # zot source so the live rebuild lane can rebuild the agent (Go) too, not
    # just Colibri (Rust). Skipped automatically if the zot checkout is absent.
    resolve_zot_paths
    seed_live_ai_source_repo "${_resolved_zot_repo}" "zot"
    # Hermes Agent source — the agent CLI + gateway for Telegram bot and
    # agent-to-agent relay. Skipped silently if the checkout is absent.
    seed_live_ai_source_repo "${HERMES_REPO:-/home/clawdie/ai/hermes-bsd}" "hermes-bsd"
    chroot "${MOUNT_POINT}" chown -R clawdie:clawdie /home/clawdie/ai
}

configure_live_operator_session() {
    echo "    Configuring live operator session..."

    mkdir -p "${MOUNT_POINT}/usr/local/bin"
    install -m 0755 "${LIVE_SESSION_DIR}/clawdie-bootstrap-launch.sh" \
        "${MOUNT_POINT}/usr/local/bin/clawdie-bootstrap-launch.sh"
    install -m 0755 "${LIVE_SESSION_DIR}/clawdie-noblank-guard.sh" \
        "${MOUNT_POINT}/usr/local/bin/clawdie-noblank-guard.sh"
    install -m 0755 "${LIVE_SESSION_DIR}/hw-report" \
        "${MOUNT_POINT}/usr/local/bin/hw-report"
    install -m 0755 "${LIVE_SESSION_DIR}/clawdie-hw-probe" \
        "${MOUNT_POINT}/usr/local/bin/clawdie-hw-probe"
    # Vaultwarden secret bridge (bw -> .env). Always staged: needs only the
    # bundled `bw` CLI and a 0600 bootstrap drop; absent bootstrap = no-op, so
    # the manual setup wizard stays the floor. See docs/VAULTWARDEN-SETUP.md.
    install -m 0755 "${LIVE_SESSION_DIR}/clawdie-vault-fetch" \
        "${MOUNT_POINT}/usr/local/bin/clawdie-vault-fetch"

    # The stock FreeBSD memstick starts bsdinstall from /etc/rc.local before
    # our graphical live session can own the USB workflow. Preserve a copy for
    # debugging, but disable the automatic text installer.
    if [ -f "${MOUNT_POINT}/etc/rc.local" ]; then
        cp "${MOUNT_POINT}/etc/rc.local" "${MOUNT_POINT}/etc/rc.local.freebsd-installer"
        rm -f "${MOUNT_POINT}/etc/rc.local"
    fi

    # The installer memstick may leave /etc/resolv.conf as a symlink into
    # /tmp/bsdinstall_etc. The live image mounts /tmp as tmpfs, so that link
    # becomes dangling on boot and DNS fails despite working DHCP/routing.
    # Ship a real file, then let DHCP/NetworkMgr replace it at runtime.
    if [ -L "${MOUNT_POINT}/etc/resolv.conf" ] && \
        readlink "${MOUNT_POINT}/etc/resolv.conf" | grep -q '^/tmp/bsdinstall_etc/'; then
        rm -f "${MOUNT_POINT}/etc/resolv.conf"
    fi
    if [ ! -e "${MOUNT_POINT}/etc/resolv.conf" ]; then
        {
            echo '# Created by the Clawdie live image build.'
            echo '# DHCP/NetworkMgr may replace this file after interfaces come up.'
        } > "${MOUNT_POINT}/etc/resolv.conf"
        chmod 0644 "${MOUNT_POINT}/etc/resolv.conf"
    fi

    # python3 → 3.11 (FreeBSD's PYTHON_DEFAULT — don't fight it). python312 is
    # also installed and stays available as python3.12 for anything needing newer.
    # FreeBSD ships no bare python3 symlink; scripts use /usr/bin/env python3.
    if [ ! -e "${MOUNT_POINT}/usr/local/bin/python3" ]; then
        if [ -x "${MOUNT_POINT}/usr/local/bin/python3.11" ]; then
            py_bin=python3.11
        else
            py_bin=$(ls "${MOUNT_POINT}/usr/local/bin"/python3.* 2>/dev/null \
                | sed 's@.*/@@' | grep -E '^python3\.[0-9]+$' | sort -V | head -1)
        fi
        if [ -n "${py_bin}" ]; then
            ln -sf "${py_bin}" "${MOUNT_POINT}/usr/local/bin/python3"
            ln -sf "${py_bin}" "${MOUNT_POINT}/usr/local/bin/python"
        fi
    fi

    mkdir -p "${MOUNT_POINT}/usr/local/etc/sddm.conf.d"
    install -m 0644 "${LIVE_SESSION_DIR}/sddm.conf" \
        "${MOUNT_POINT}/usr/local/etc/sddm.conf.d/50-clawdie-live.conf"
    mkdir -p "${MOUNT_POINT}/var/lib/sddm" "${MOUNT_POINT}/var/log"
    cat > "${MOUNT_POINT}/var/lib/sddm/state.conf" <<'EOF'
[Last]
Session=clawdie-xfce.desktop
User=clawdie
EOF
    chmod 0755 "${MOUNT_POINT}/var/lib/sddm"
    chmod 0644 "${MOUNT_POINT}/var/lib/sddm/state.conf"
    if chroot "${MOUNT_POINT}" /usr/sbin/pw usershow sddm >/dev/null 2>&1; then
        chroot "${MOUNT_POINT}" chown -R sddm:sddm /var/lib/sddm
    fi
    mkdir -p "${MOUNT_POINT}/usr/local/share/xsessions"
    install -m 0644 "${LIVE_SESSION_DIR}/clawdie-xfce.desktop" \
        "${MOUNT_POINT}/usr/local/share/xsessions/clawdie-xfce.desktop"

    # Strip any Wayland session .desktop files dropped by transitive
    # packages. The operator USB has no working Wayland compositor
    # (XFCE on FreeBSD is X11-only here), so offering a Wayland entry
    # in the SDDM greeter just hands the operator a non-booting session.
    if [ -d "${MOUNT_POINT}/usr/local/share/wayland-sessions" ]; then
        rm -f "${MOUNT_POINT}/usr/local/share/wayland-sessions/"*.desktop
        rmdir "${MOUNT_POINT}/usr/local/share/wayland-sessions" 2>/dev/null || true
    fi
    mkdir -p "${MOUNT_POINT}/usr/local/etc/xdg/autostart"
    install -m 0644 "${LIVE_SESSION_DIR}/autostart/clawdie-bootstrap.desktop" \
        "${MOUNT_POINT}/usr/local/etc/xdg/autostart/clawdie-bootstrap.desktop"
    install -m 0644 "${LIVE_SESSION_DIR}/autostart/clawdie-noblank-guard.desktop" \
        "${MOUNT_POINT}/usr/local/etc/xdg/autostart/clawdie-noblank-guard.desktop"
    install -m 0644 "${LIVE_SESSION_DIR}/autostart/volumeicon.desktop" \
        "${MOUNT_POINT}/usr/local/etc/xdg/autostart/volumeicon.desktop"
    mkdir -p "${MOUNT_POINT}/usr/local/etc/X11/xorg.conf.d"
    install -m 0644 "${LIVE_SESSION_DIR}/xorg.conf.d/30-keyboard.conf" \
        "${MOUNT_POINT}/usr/local/etc/X11/xorg.conf.d/30-keyboard.conf"
    install -m 0644 "${LIVE_SESSION_DIR}/xorg.conf.d/40-clawdie-noblank.conf" \
        "${MOUNT_POINT}/usr/local/etc/X11/xorg.conf.d/40-clawdie-noblank.conf"
    mkdir -p "${MOUNT_POINT}/usr/local/etc/rc.d"
    install -m 0755 "${LIVE_SESSION_DIR}/clawdie-live-gpu" \
        "${MOUNT_POINT}/usr/local/etc/rc.d/clawdie_live_gpu"
    install -m 0755 "${LIVE_SESSION_DIR}/clawdie-live-wifi" \
        "${MOUNT_POINT}/usr/local/etc/rc.d/clawdie_live_wifi"
    install -m 0755 "${LIVE_SESSION_DIR}/clawdie-live-seed" \
        "${MOUNT_POINT}/usr/local/etc/rc.d/clawdie_live_seed"
    install -m 0755 "${LIVE_SESSION_DIR}/clawdie-firstboot-rootpw" \
        "${MOUNT_POINT}/usr/local/etc/rc.d/clawdie_firstboot_rootpw"
    install -m 0755 "${LIVE_SESSION_DIR}/clawdie-live-resolver" \
        "${MOUNT_POINT}/usr/local/etc/rc.d/clawdie_live_resolver"
    install -m 0755 "${LIVE_SESSION_DIR}/clawdie-live-audio" \
        "${MOUNT_POINT}/usr/local/etc/rc.d/clawdie_live_audio"
    install -m 0755 "${LIVE_SESSION_DIR}/clawdie-live-power" \
        "${MOUNT_POINT}/usr/local/etc/rc.d/clawdie_live_power"
    mkdir -p "${MOUNT_POINT}/usr/local/etc/polkit-1/rules.d"
    install -m 0644 "${LIVE_SESSION_DIR}/49-clawdie-power.rules" \
        "${MOUNT_POINT}/usr/local/etc/polkit-1/rules.d/49-clawdie-power.rules"
    install -m 0755 "${LIVE_SESSION_DIR}/clawdie-xfce-session" \
        "${MOUNT_POINT}/usr/local/bin/clawdie-xfce-session"
    install -m 0755 "${LIVE_SESSION_DIR}/clawdie-xfce-session-inner" \
        "${MOUNT_POINT}/usr/local/bin/clawdie-xfce-session-inner"
    install -m 0755 "${LIVE_SESSION_DIR}/clawdie-live-touchpad-guard" \
        "${MOUNT_POINT}/usr/local/bin/clawdie-live-touchpad-guard"
    install -m 0755 "${LIVE_SESSION_DIR}/clawdie-startx" \
        "${MOUNT_POINT}/usr/local/bin/clawdie-startx"
    install -m 0755 "${LIVE_SESSION_DIR}/clawdie-startx" \
        "${MOUNT_POINT}/usr/local/bin/clawdie-gui"
    for _xfce_xdg_dir in \
        "${MOUNT_POINT}/usr/local/etc" \
        "${MOUNT_POINT}/usr/local/etc/xdg" \
        "${MOUNT_POINT}/usr/local/etc/xdg/xfce4"; do
        if [ ! -d "$_xfce_xdg_dir" ]; then
            echo "ERROR: XFCE XDG directory missing from live image: ${_xfce_xdg_dir#${MOUNT_POINT}}"
            exit 1
        fi
        chmod 0755 "$_xfce_xdg_dir"
    done
    if [ -f "${MOUNT_POINT}/usr/local/etc/xdg/xfce4/xinitrc" ]; then
        chmod 0755 "${MOUNT_POINT}/usr/local/etc/xdg/xfce4/xinitrc"
    else
        echo "ERROR: XFCE xinitrc missing from live image"
        exit 1
    fi
    if [ ! -x "${MOUNT_POINT}/usr/local/bin/xinit" ] || [ ! -x "${MOUNT_POINT}/usr/local/bin/startx" ]; then
        echo "ERROR: xinit/startx missing from live image"
        exit 1
    fi
    if [ ! -x "${MOUNT_POINT}/usr/local/bin/clawdie-startx" ]; then
        echo "ERROR: clawdie-startx rescue launcher missing from live image"
        exit 1
    fi
    if [ ! -x "${MOUNT_POINT}/usr/local/bin/bash" ]; then
        echo "ERROR: bash missing from live image"
        exit 1
    fi
    if [ ! -x "${MOUNT_POINT}/usr/local/bin/zsh" ]; then
        echo "ERROR: zsh missing from live image"
        exit 1
    fi
    if ! grep -qx '/usr/local/bin/bash' "${MOUNT_POINT}/etc/shells" 2>/dev/null; then
        printf '%s\n' '/usr/local/bin/bash' >> "${MOUNT_POINT}/etc/shells"
    fi
    if ! grep -qx '/usr/local/bin/zsh' "${MOUNT_POINT}/etc/shells" 2>/dev/null; then
        printf '%s\n' '/usr/local/bin/zsh' >> "${MOUNT_POINT}/etc/shells"
    fi

    if ! /usr/sbin/pw -R "$MOUNT_POINT" usershow clawdie >/dev/null 2>&1; then
        /usr/sbin/pw -R "$MOUNT_POINT" useradd clawdie \
            -m \
            -s /usr/local/bin/bash \
            -c "Clawdie Operator"
    fi
    /usr/sbin/pw -R "$MOUNT_POINT" usermod clawdie -s /usr/local/bin/bash
    /usr/sbin/pw -R "$MOUNT_POINT" groupmod wheel -m clawdie
    if /usr/sbin/pw -R "$MOUNT_POINT" groupshow video >/dev/null 2>&1; then
        /usr/sbin/pw -R "$MOUNT_POINT" groupmod video -m clawdie
    fi
    CLAWDIE_UID="$(/usr/sbin/pw -R "$MOUNT_POINT" usershow clawdie | cut -d: -f3)"
    # NOTE: chown of /opt/clawdie/npm-global was moved into
    # install_live_npm_globals (called later in the build). The previous
    # chown here ran before npm globals were installed and silently
    # chowned two empty directories, leaving the package tree root:wheel
    # at runtime. Do not re-introduce a chown here.
    if [ -n "${CLAWDIE_USER_PASSWORD:-}" ]; then
        printf '%s\n' "${CLAWDIE_USER_PASSWORD}" | /usr/sbin/pw -R "$MOUNT_POINT" usermod clawdie -h 0
    fi

    # Do not force vendor-specific DRM firmware or KMS drivers through
    # loader.conf. The live image must boot Intel, AMD, VMware, NVIDIA and
    # fallback framebuffer hosts from the same rootfs. GPU KMS is selected at
    # boot by /usr/local/etc/rc.d/clawdie_live_gpu after inspecting display PCI
    # vendor IDs.

    # Live root is on USB. Avoid USB autosuspend/resume surprises while the
    # root filesystem is attached to the same bus.
    set_config_line "${MOUNT_POINT}/boot/loader.conf" 'hw.usb.no_suspend="1"'

    # The stock installer memstick brands the loader menu as "Installer".
    # This artifact is an operator live USB, so use generic FreeBSD branding
    # plus an explicit Clawdie live prompt.
    set_config_line "${MOUNT_POINT}/boot/loader.conf" 'loader_brand="orb"'
    set_config_line "${MOUNT_POINT}/boot/loader.conf" 'loader_menu_title="Clawdie Operator USB"'
    set_config_line "${MOUNT_POINT}/boot/loader.conf" 'loader_menu_multi_user_prompt="Start Clawdie Operator USB"'

    # Enable FreeBSD mac_do for kernel-enforced privilege escalation.
    # Base system only — no sudo package needed. Wheel group members can
    # become root via mdo(1). mdo -u root changes the primary gid to wheel,
    # so the target rule must explicitly allow gid=0 as well as uid=0.
    set_config_line "${MOUNT_POINT}/boot/loader.conf" 'mac_do_load="YES"'
    set_config_line "${MOUNT_POINT}/etc/sysctl.conf" 'security.mac.do.rules=gid=0>uid=0,gid=0,+gid=*'

    if ! grep -q '^\[clawdie_live=' "${MOUNT_POINT}/etc/devfs.rules" 2>/dev/null; then
        cat >> "${MOUNT_POINT}/etc/devfs.rules" <<'EOF'

[clawdie_live=10]
add path 'dri' mode 0755 group video
add path 'dri/*' mode 0660 group video
add path 'drm/*' mode 0660 group video
EOF
    fi
    set_config_line "${MOUNT_POINT}/etc/rc.conf" 'devfs_system_ruleset="clawdie_live"'

    _nm_autostart="${MOUNT_POINT}/usr/local/etc/xdg/autostart/networkmgr.desktop"
    if [ ! -f "$_nm_autostart" ]; then
        echo "ERROR: networkmgr.desktop missing from live image"
        exit 1
    fi
    if /usr/local/sbin/pkg -r "$MOUNT_POINT" info -e sudo 2>/dev/null; then
        echo "ERROR: sudo is installed in the live image"
        exit 1
    fi
    if [ -e "${MOUNT_POINT}/usr/local/etc/sudoers.d/networkmgr" ]; then
        echo "ERROR: NetworkMgr sudoers policy is still present in the live image"
        exit 1
    fi
    if ! grep -q 'Exec=mdo -u root networkmgr' "$_nm_autostart"; then
        echo "ERROR: networkmgr.desktop does not use mdo -u root networkmgr"
        exit 1
    fi
    if /usr/local/sbin/pkg -r "$MOUNT_POINT" info -d networkmgr 2>/dev/null | grep -q '^.*sudo-'; then
        echo "ERROR: networkmgr still depends on sudo in the live image"
        exit 1
    fi

    mkdir -p "${MOUNT_POINT}/home/clawdie" "${MOUNT_POINT}/etc/profile.d" "${MOUNT_POINT}/etc/skel"

    # Install the canonical npm-global profile snippet (shared with agent-jail-bootstrap.sh).
    # The snippet lives in colibri; resolve its repo the same way every other
    # colibri consumer here does so COLIBRI_REPO overrides are honored.
    resolve_colibri_paths
    _npm_profile_src="${_resolved_colibri_repo}/packaging/freebsd/clawdie-npm-profile.sh"
    if [ ! -f "${_npm_profile_src}" ]; then
        echo "ERROR: shared npm profile snippet missing: ${_npm_profile_src}"
        echo "       It ships in colibri packaging/freebsd/. Set COLIBRI_REPO=/path/to/colibri."
        exit 1
    fi
    {
        printf 'NPM_PREFIX="/opt/clawdie/npm-global"\n'
        cat "${_npm_profile_src}"
    } > "${MOUNT_POINT}/etc/profile.d/clawdie-npm.sh"
    chmod 0644 "${MOUNT_POINT}/etc/profile.d/clawdie-npm.sh"

    # ISO-specific environment (PATH, locale, colibri socket). Sources the
    # shared npm snippet rather than duplicating it.
    cat > "${MOUNT_POINT}/etc/profile.d/clawdie.sh" <<'EOF'
# Clawdie live operator environment.
_clawdie_base_path="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
_clawdie_path_has() {
    case ":${PATH:-}:" in
        *:"$1":*) return 0 ;;
        *) return 1 ;;
    esac
}
export LANG="${LANG:-en_US.UTF-8}"
export LC_ALL="${LC_ALL:-en_US.UTF-8}"
if [ -z "${PATH:-}" ]; then
    PATH="${_clawdie_base_path}"
else
    for _clawdie_dir in /usr/local/sbin /usr/local/bin /usr/sbin /usr/bin /sbin /bin; do
        _clawdie_path_has "${_clawdie_dir}" || PATH="${PATH}:${_clawdie_dir}"
    done
fi
# npm-global bin + npm config — canonical snippet shared with agent jails
[ -r /etc/profile.d/clawdie-npm.sh ] && . /etc/profile.d/clawdie-npm.sh
export PATH

# Colibri daemon socket — needed for 'colibri' CLI without --socket
export COLIBRI_DAEMON_SOCKET="/var/run/colibri/colibri.sock"

unset _clawdie_base_path
unset -f _clawdie_path_has 2>/dev/null || true
EOF
    chmod 0644 "${MOUNT_POINT}/etc/profile.d/clawdie.sh"
    cat > "${MOUNT_POINT}/home/clawdie/.profile" <<'EOF'
# Clawdie operator POSIX shell profile.
[ -r /etc/profile.d/clawdie.sh ] && . /etc/profile.d/clawdie.sh
EOF
    cat > "${MOUNT_POINT}/home/clawdie/.bash_profile" <<'EOF'
# Clawdie operator bash login profile.
[ -r /etc/profile ] && . /etc/profile
[ -L "${HOME}/.cache" ] && {
    mkdir -p /tmp/clawdie/cache 2>/dev/null || true
    chown "$(id -u):$(id -g)" /tmp/clawdie /tmp/clawdie/cache 2>/dev/null || true
    chmod 0700 /tmp/clawdie /tmp/clawdie/cache 2>/dev/null || true
}
[ -r "${HOME}/.bashrc" ] && . "${HOME}/.bashrc"
EOF
    cat > "${MOUNT_POINT}/home/clawdie/.bashrc" <<'EOF'
# Clawdie operator interactive bash profile.
[ -r /etc/profile.d/clawdie.sh ] && . /etc/profile.d/clawdie.sh
if [ -n "${PS1:-}" ]; then
    export HISTFILE="${HISTFILE:-/tmp/clawdie/bash_history}"
    mkdir -p /tmp/clawdie 2>/dev/null || true
fi
EOF
    cat > "${MOUNT_POINT}/home/clawdie/.zprofile" <<'EOF'
# Clawdie operator zsh login profile.
[ -r /etc/profile ] && . /etc/profile
[ -L "${HOME}/.cache" ] && {
    mkdir -p /tmp/clawdie/cache 2>/dev/null || true
    chown "$(id -u):$(id -g)" /tmp/clawdie /tmp/clawdie/cache 2>/dev/null || true
    chmod 0700 /tmp/clawdie /tmp/clawdie/cache 2>/dev/null || true
}
[ -r /etc/profile.d/clawdie.sh ] && . /etc/profile.d/clawdie.sh
[ -r "${HOME}/.zshrc" ] && . "${HOME}/.zshrc"
EOF
    cat > "${MOUNT_POINT}/home/clawdie/.zshrc" <<'EOF'
# Clawdie operator interactive zsh profile.
[ -r /etc/profile.d/clawdie.sh ] && . /etc/profile.d/clawdie.sh
export HISTFILE="${HISTFILE:-/tmp/clawdie/zsh_history}"
mkdir -p /tmp/clawdie 2>/dev/null || true

# Keep zsh optional, but ready: use packaged oh-my-zsh when present.
if [ -d /usr/local/share/ohmyzsh ]; then
    export ZSH="/usr/local/share/ohmyzsh"
elif [ -d /usr/local/share/oh-my-zsh ]; then
    export ZSH="/usr/local/share/oh-my-zsh"
fi
if [ -n "${ZSH:-}" ] && [ -r "${ZSH}/oh-my-zsh.sh" ]; then
    ZSH_THEME="${ZSH_THEME:-robbyrussell}"
    plugins=(git)
    source "${ZSH}/oh-my-zsh.sh"
fi
EOF
    cat > "${MOUNT_POINT}/home/clawdie/.tmux.conf" <<'EOF'
# Clawdie operator tmux defaults.
# Pi uses modified Enter/key chords; tmux must pass CSI-u extended keys or Pi
# warns that modified Enter keys may not work.
set -g extended-keys on
set -g extended-keys-format csi-u
set -g escape-time 10
set -g mouse on
set -g base-index 1
setw -g pane-base-index 1
set -g renumber-windows on
EOF
    mkdir -p "${MOUNT_POINT}/usr/local/etc"
    cp "${MOUNT_POINT}/home/clawdie/.tmux.conf" "${MOUNT_POINT}/usr/local/etc/tmux.conf"
    cp "${MOUNT_POINT}/home/clawdie/.profile" "${MOUNT_POINT}/etc/skel/.profile"
    cp "${MOUNT_POINT}/home/clawdie/.bash_profile" "${MOUNT_POINT}/etc/skel/.bash_profile"
    cp "${MOUNT_POINT}/home/clawdie/.bashrc" "${MOUNT_POINT}/etc/skel/.bashrc"
    cp "${MOUNT_POINT}/home/clawdie/.zprofile" "${MOUNT_POINT}/etc/skel/.zprofile"
    cp "${MOUNT_POINT}/home/clawdie/.zshrc" "${MOUNT_POINT}/etc/skel/.zshrc"
    cp "${MOUNT_POINT}/home/clawdie/.tmux.conf" "${MOUNT_POINT}/etc/skel/.tmux.conf"
    chmod 0644 \
        "${MOUNT_POINT}/home/clawdie/.profile" \
        "${MOUNT_POINT}/home/clawdie/.bash_profile" \
        "${MOUNT_POINT}/home/clawdie/.bashrc" \
        "${MOUNT_POINT}/home/clawdie/.zprofile" \
        "${MOUNT_POINT}/home/clawdie/.zshrc" \
        "${MOUNT_POINT}/home/clawdie/.tmux.conf" \
        "${MOUNT_POINT}/usr/local/etc/tmux.conf" \
        "${MOUNT_POINT}/etc/skel/.profile" \
        "${MOUNT_POINT}/etc/skel/.bash_profile" \
        "${MOUNT_POINT}/etc/skel/.bashrc" \
        "${MOUNT_POINT}/etc/skel/.zprofile" \
        "${MOUNT_POINT}/etc/skel/.zshrc" \
        "${MOUNT_POINT}/etc/skel/.tmux.conf"
    install -m 0755 "${LIVE_SESSION_DIR}/xprofile" \
        "${MOUNT_POINT}/home/clawdie/.xprofile"
    cat > "${MOUNT_POINT}/home/clawdie/.xinitrc" <<'EOF'
#!/bin/sh
exec /usr/local/bin/clawdie-xfce-session
EOF
    mkdir -p "${MOUNT_POINT}/home/clawdie/.config/xfce4" "${MOUNT_POINT}/etc/skel/.config/xfce4"
    cp "${MOUNT_POINT}/home/clawdie/.xinitrc" "${MOUNT_POINT}/home/clawdie/.config/xfce4/xinitrc"
    cp "${MOUNT_POINT}/home/clawdie/.xinitrc" "${MOUNT_POINT}/etc/skel/.xinitrc"
    cp "${MOUNT_POINT}/home/clawdie/.xinitrc" "${MOUNT_POINT}/etc/skel/.config/xfce4/xinitrc"

    # Pre-stage mother connectivity key if present on the build host.
    # Lets the live USB node SSH into the mother server (osa) without
    # manual key exchange. Public key is already in mother authorized_keys.
    _mother_key_src="/home/clawdie/.ssh/mother-mcp"
    if [ -f "${_mother_key_src}" ]; then
        [ "${BUILD_CHANNEL}" = "release" ] && { echo "ERROR: refusing to bake mother SSH key into a release image"; exit 1; }

        mkdir -p "${MOUNT_POINT}/home/clawdie/.ssh"
        cp "${_mother_key_src}" "${MOUNT_POINT}/home/clawdie/.ssh/mother-mcp"
        chmod 0600 "${MOUNT_POINT}/home/clawdie/.ssh/mother-mcp"
        # Also install for the daemon user (colibri) — same key, different home.
        mkdir -p "${MOUNT_POINT}/var/db/colibri/.ssh"
        cp "${_mother_key_src}" "${MOUNT_POINT}/var/db/colibri/.ssh/mother-mcp"
        chown -R colibri:colibri "${MOUNT_POINT}/var/db/colibri/.ssh" 2>/dev/null || true
        chmod 0600 "${MOUNT_POINT}/var/db/colibri/.ssh/mother-mcp"
        echo "    Staged mother SSH key for USB→mother connectivity."
    fi
    chmod 0755 \
        "${MOUNT_POINT}/home/clawdie/.xinitrc" \
        "${MOUNT_POINT}/home/clawdie/.config/xfce4/xinitrc" \
        "${MOUNT_POINT}/etc/skel/.xinitrc" \
        "${MOUNT_POINT}/etc/skel/.config/xfce4/xinitrc"
    if [ -f "${MOUNT_POINT}/usr/local/etc/xdg/tumbler/tumbler.rc" ]; then
        mkdir -p "${MOUNT_POINT}/home/clawdie/.config/tumbler" "${MOUNT_POINT}/etc/skel/.config/tumbler"
        install -m 0644 "${MOUNT_POINT}/usr/local/etc/xdg/tumbler/tumbler.rc" \
            "${MOUNT_POINT}/home/clawdie/.config/tumbler/tumbler.rc"
        install -m 0644 "${MOUNT_POINT}/usr/local/etc/xdg/tumbler/tumbler.rc" \
            "${MOUNT_POINT}/etc/skel/.config/tumbler/tumbler.rc"
    fi
    chroot "$MOUNT_POINT" chown -R clawdie:clawdie /home/clawdie

    mkdir -p "${MOUNT_POINT}/usr/local/share/applications"
    install -m 0644 "${LIVE_SESSION_DIR}/clawdie-bootstrap.desktop" \
        "${MOUNT_POINT}/usr/local/share/applications/Clawdie Start Here.desktop"
    install -m 0644 "${LIVE_SESSION_DIR}/colibri-dashboard.desktop" \
        "${MOUNT_POINT}/usr/local/share/applications/Colibri Dashboard.desktop"
    mkdir -p "${MOUNT_POINT}/home/clawdie/Desktop"
    install -m 0644 "${LIVE_SESSION_DIR}/clawdie-bootstrap.desktop" \
        "${MOUNT_POINT}/home/clawdie/Desktop/Clawdie Start Here.desktop"
    install -m 0644 "${LIVE_SESSION_DIR}/colibri-dashboard.desktop" \
        "${MOUNT_POINT}/home/clawdie/Desktop/Colibri Dashboard.desktop"
    mkdir -p "${MOUNT_POINT}/usr/local/share/clawdie-iso"
    install -m 0644 "${LIVE_SESSION_DIR}/START-HERE.txt" \
        "${MOUNT_POINT}/usr/local/share/clawdie-iso/START-HERE.txt"
    install -m 0755 "${LIVE_SESSION_DIR}/colibri-panel-indicator.sh" \
        "${MOUNT_POINT}/usr/local/bin/colibri-panel-indicator"
    install -m 0755 "${LIVE_SESSION_DIR}/clawdie-wallpaper-gen.sh" \
        "${MOUNT_POINT}/usr/local/bin/clawdie-wallpaper-gen"
    install -m 0755 "${LIVE_SESSION_DIR}/clawdie-join-hive.sh" \
        "${MOUNT_POINT}/usr/local/bin/clawdie-join-hive"
    install -m 0644 "${LIVE_SESSION_DIR}/clawdie-join-hive.desktop" \
        "${MOUNT_POINT}/usr/local/share/applications/Clawdie Join Hive.desktop"
    install -m 0644 "${LIVE_SESSION_DIR}/clawdie-join-hive.desktop" \
        "${MOUNT_POINT}/home/clawdie/Desktop/Join Hive.desktop"
    install -m 0755 "${LIVE_SESSION_DIR}/clawdie-enable-mother.sh" \
        "${MOUNT_POINT}/usr/local/bin/clawdie-enable-mother"
    install -m 0644 "${LIVE_SESSION_DIR}/clawdie-enable-mother.desktop" \
        "${MOUNT_POINT}/usr/local/share/applications/Clawdie Enable Mother.desktop"
    install -m 0644 "${LIVE_SESSION_DIR}/clawdie-enable-mother.desktop" \
        "${MOUNT_POINT}/home/clawdie/Desktop/Enable Mother Link.desktop"
    mkdir -p "${MOUNT_POINT}/usr/local/share/clawdie-iso/bootstrap"
    install -m 0644 "${LIVE_SESSION_DIR}/bootstrap.html" \
        "${MOUNT_POINT}/usr/local/share/clawdie-iso/bootstrap/index.html"
    mkdir -p "${MOUNT_POINT}/usr/local/share/clawdie-iso/seed"
    install -m 0644 "${LIVE_SESSION_DIR}/clawdie-live-seed.README.txt" \
        "${MOUNT_POINT}/usr/local/share/clawdie-iso/seed/README.txt"
    if [ -d "${LIVE_SESSION_DIR}/mcp-examples" ]; then
        mkdir -p "${MOUNT_POINT}/usr/local/share/clawdie-iso/mcp-examples"
        cp -R "${LIVE_SESSION_DIR}/mcp-examples/." \
            "${MOUNT_POINT}/usr/local/share/clawdie-iso/mcp-examples/"
        find "${MOUNT_POINT}/usr/local/share/clawdie-iso/mcp-examples" \
            -type f -exec chmod 0644 {} +
    fi
    chroot "$MOUNT_POINT" chown -R clawdie:clawdie /home/clawdie/Desktop
    chmod 0755 "${MOUNT_POINT}/home/clawdie/Desktop"
    chmod 0644 \
        "${MOUNT_POINT}/home/clawdie/Desktop/Clawdie Start Here.desktop" \
        "${MOUNT_POINT}/home/clawdie/Desktop/Colibri Dashboard.desktop"

    install_live_ai_source_snapshots

    # The stock FreeBSD install memstick is intentionally read-only. This live
    # operator USB needs a writable root so SDDM, Xorg, NetworkMgr, logs, and
    # operator diagnostics can create runtime state on the flashed stick.
    set_fstab_line "${MOUNT_POINT}/etc/fstab" '^[[:space:]]*/dev/ufs/FreeBSD_Install[[:space:]]+/[[:space:]]+ufs[[:space:]]' '/dev/ufs/FreeBSD_Install / ufs rw,noatime 1 1'
    if ! grep -Eq '^[[:space:]]*/dev/ufs/FreeBSD_Install[[:space:]]+/[[:space:]]+ufs[[:space:]]+([^[:space:]]*,)?rw(,|[[:space:]])' "${MOUNT_POINT}/etc/fstab"; then
        echo "ERROR: live USB root filesystem is not configured read-write in /etc/fstab"
        exit 1
    fi
    ensure_fstab_line "${MOUNT_POINT}/etc/fstab" '^[[:space:]]*proc[[:space:]]+/proc[[:space:]]+procfs[[:space:]]' 'proc /proc procfs rw 0 0'
    ensure_fstab_line "${MOUNT_POINT}/etc/fstab" '^[[:space:]]*tmpfs[[:space:]]+/tmp[[:space:]]+tmpfs[[:space:]]' 'tmpfs /tmp tmpfs rw,mode=1777 0 0'
    ensure_fstab_line "${MOUNT_POINT}/etc/fstab" '^[[:space:]]*tmpfs[[:space:]]+/var/log[[:space:]]+tmpfs[[:space:]]' 'tmpfs /var/log tmpfs rw,mode=755 0 0'

    # No swap on the live USB. USB write wear, unpredictable behaviour, and
    # it would defeat the tmpfs work above (under memory pressure the kernel
    # would page tmpfs contents to swap, putting writes back on the stick).
    # On an 8 GB RAM baseline, "fail visibly under memory pressure" is the
    # right behaviour, not "silently grind the USB". Strip any inherited
    # swap entries and assert clean.
    sed -i '' '/^[[:space:]]*[^#].*[[:space:]]swap[[:space:]]/d' "${MOUNT_POINT}/etc/fstab"
    if grep -qE '^[[:space:]]*[^#].*[[:space:]]swap[[:space:]]' "${MOUNT_POINT}/etc/fstab"; then
        echo "ERROR: swap entry remains in live image /etc/fstab"
        exit 1
    fi

    set_config_line "${MOUNT_POINT}/etc/rc.conf" 'root_rw_mount="YES"'
    # FreeBSD default dumpdev_enable="AUTO" scans for swap on boot to use as
    # the kernel crash-dump device. With no swap and no desire to crash-dump
    # to the USB anyway, set NO explicitly so the scan never runs.
    set_config_line "${MOUNT_POINT}/etc/rc.conf" 'dumpdev_enable="NO"'
    # The stock installer memstick overlays /tmp and /var with tiny tmpfs
    # filesystems. That hides image content such as /var/lib/xkb and leaves
    # too little runtime space for a graphical desktop. The operator USB has a
    # writable root filesystem, so keep /tmp and /var on the root UFS image.
    set_config_line "${MOUNT_POINT}/etc/rc.conf" 'tmpmfs="NO"'
    set_config_line "${MOUNT_POINT}/etc/rc.conf" 'varmfs="NO"'
    if ! grep -Eq '^root_rw_mount="YES"' "${MOUNT_POINT}/etc/rc.conf"; then
        echo "ERROR: live USB root_rw_mount is not enabled in /etc/rc.conf"
        exit 1
    fi
    if ! grep -Eq '^tmpmfs="NO"' "${MOUNT_POINT}/etc/rc.conf" || ! grep -Eq '^varmfs="NO"' "${MOUNT_POINT}/etc/rc.conf"; then
        echo "ERROR: live USB tmpmfs/varmfs overlays are not disabled in /etc/rc.conf"
        exit 1
    fi
    # Keep a real hostname so startx/xauth do not derive invalid display names
    # such as bare :0 or /unix:0 on the installer-derived "Amnesiac" profile.
    set_config_line "${MOUNT_POINT}/etc/rc.conf" 'hostname="usb.clawdie.home.arpa"'
    if ! grep -Eq '^hostname="usb.clawdie.home.arpa"' "${MOUNT_POINT}/etc/rc.conf"; then
        echo "ERROR: live USB hostname is not configured in /etc/rc.conf"
        exit 1
    fi
    ensure_fstab_line "${MOUNT_POINT}/etc/hosts" '^[[:space:]]*127\.0\.0\.1[[:space:]].*clawdie-live' '127.0.0.1 clawdie-live'
    ensure_fstab_line "${MOUNT_POINT}/etc/hosts" '^[[:space:]]*::1[[:space:]].*clawdie-live' '::1 clawdie-live'
    configure_nsswitch_hosts_line "${MOUNT_POINT}/etc/nsswitch.conf"
    set_config_line "${MOUNT_POINT}/etc/rc.conf" 'dbus_enable="YES"'
    set_config_line "${MOUNT_POINT}/etc/rc.conf" 'sshd_enable="YES"'
    set_config_line "${MOUNT_POINT}/etc/rc.conf" 'tailscaled_enable="YES"'
    set_config_line "${MOUNT_POINT}/etc/rc.conf" 'avahi_daemon_enable="YES"'
    set_config_line "${MOUNT_POINT}/etc/rc.conf" 'sddm_enable="YES"'
    set_config_line "${MOUNT_POINT}/etc/rc.conf" 'display_manager="sddm"'
    set_config_line "${MOUNT_POINT}/etc/rc.conf" 'clawdie_live_gpu_enable="YES"'
    set_config_line "${MOUNT_POINT}/etc/rc.conf" 'clawdie_live_gpu_mode="auto"'
    set_config_line "${MOUNT_POINT}/etc/rc.conf" 'clawdie_live_gpu_nvidia_branch=""'
    set_config_line "${MOUNT_POINT}/etc/rc.conf" 'clawdie_live_wifi_enable="YES"'
    set_config_line "${MOUNT_POINT}/etc/rc.conf" 'clawdie_live_seed_enable="YES"'
    set_config_line "${MOUNT_POINT}/etc/rc.conf" 'clawdie_firstboot_rootpw_enable="YES"'
    set_config_line "${MOUNT_POINT}/etc/rc.conf" 'clawdie_live_resolver_enable="YES"'
    set_config_line "${MOUNT_POINT}/etc/rc.conf" 'clawdie_live_audio_enable="YES"'
    set_config_line "${MOUNT_POINT}/etc/rc.conf" 'clawdie_live_power_enable="YES"'
    set_config_line "${MOUNT_POINT}/etc/rc.conf" 'webcamd_enable="YES"'
    # CPU power management. powerdxx is a drop-in replacement for base
    # powerd with moving-average load sampling and better multi-core
    # behavior (matters on Ryzen 7 5700U-class chips). Do NOT also set
    # powerd_enable — both daemons use the same pidfile and one would
    # fail to start. Base powerd defaults to NO when unset, so leaving
    # it unset is the right way to keep it off.
    # The bigger battery lever is the C-state pair. Use C3 rather than Cmax
    # on the live USB because root is USB-backed; deeper C6/C7-style states
    # can expose USB controller resume bugs on some Ryzen mobile systems.
    # Installed systems can choose Cmax once root is on internal storage.
    set_config_line "${MOUNT_POINT}/etc/rc.conf" 'powerdxx_enable="YES"'
    set_config_line "${MOUNT_POINT}/etc/rc.conf" 'powerdxx_flags="-a hiadaptive -b adaptive -n adaptive"'
    set_config_line "${MOUNT_POINT}/etc/rc.conf" 'performance_cx_lowest="C3"'
    set_config_line "${MOUNT_POINT}/etc/rc.conf" 'economy_cx_lowest="C3"'
    set_config_line "${MOUNT_POINT}/etc/rc.conf" 'linux_enable="YES"'
    set_config_line "${MOUNT_POINT}/etc/rc.conf" 'zfs_enable="YES"'
    set_config_line "${MOUNT_POINT}/etc/rc.conf" 'pf_enable="YES"'
    # GPU KMS is selected at boot by /usr/local/etc/rc.d/clawdie_live_gpu so
    # the live USB does not force the Intel driver on non-Intel hardware.
    # kld_list breakdown:
    #   linux / linux64           — Linuxulator binary compatibility.
    #   zfs                       — runtime ZFS support (pool import on demand).
    #   cuse                      — userspace character devices; required by webcamd.
    #   hidbus / iichid           — HID-over-I²C bridge for modern touchpads / keyboards.
    #   hms / hmt / hkbd          — HID mouse / multitouch / keyboard class drivers.
    #   acpi_video                — backlight control sysctls (hw.acpi.video.lcd0.brightness).
    append_rc_list_values "${MOUNT_POINT}/etc/rc.conf" kld_list \
        linux linux64 zfs \
        cuse \
        hidbus iichid hms hmt hkbd \
        acpi_video
    case "${GPU_DRIVER:-}" in
        nvidia-390|nvidia-470|nvidia-590)
            set_config_line "${MOUNT_POINT}/etc/rc.conf" 'clawdie_live_gpu_mode="nvidia"'
            set_config_line "${MOUNT_POINT}/etc/rc.conf" "clawdie_live_gpu_nvidia_branch=\"${GPU_DRIVER#nvidia-}\""
            append_rc_list_values "${MOUNT_POINT}/etc/rc.conf" kld_list \
                nvidia-modeset nvidia
            ;;
    esac

    # Universal NVIDIA lane: no branch is baked; clawdie_live_gpu installs the
    # matching one from the on-image repo at boot. Overrides the mode set above.
    # kld_list intentionally does NOT preload nvidia (it is not installed yet at
    # first boot); the rc.d service kldloads it after install.
    if [ "${NVIDIA_UNIVERSAL:-NO}" = "YES" ]; then
        set_config_line "${MOUNT_POINT}/etc/rc.conf" 'clawdie_live_gpu_mode="nvidia-auto"'
        set_config_line "${MOUNT_POINT}/etc/rc.conf" 'clawdie_live_gpu_nvidia_branch=""'
    fi

    mkdir -p "${MOUNT_POINT}/etc/ssh/sshd_config.d"
    install -m 0644 "${LIVE_SESSION_DIR}/sshd-live.conf" \
        "${MOUNT_POINT}/etc/ssh/sshd_config.d/clawdie-live.conf"
    ensure_sshd_include_line "${MOUNT_POINT}/etc/ssh/sshd_config"
    rm -rf "${MOUNT_POINT}/home/clawdie/.ssh"
    if [ -n "${SSH_PUBLIC_KEY:-}" ]; then
        mkdir -p "${MOUNT_POINT}/home/clawdie/.ssh"
        chmod 0700 "${MOUNT_POINT}/home/clawdie/.ssh"
        printf '%s\n' "${SSH_PUBLIC_KEY}" > "${MOUNT_POINT}/home/clawdie/.ssh/authorized_keys"
        chmod 0600 "${MOUNT_POINT}/home/clawdie/.ssh/authorized_keys"
        chroot "${MOUNT_POINT}" chown -R clawdie:clawdie /home/clawdie/.ssh
    fi
    if grep -q '^sshd_enable="YES"' "${MOUNT_POINT}/etc/rc.conf"; then
        _drop_in="${MOUNT_POINT}/etc/ssh/sshd_config.d/clawdie-live.conf"
        if [ ! -s "$_drop_in" ]; then
            echo "ERROR: sshd_enable=YES but ${_drop_in#${MOUNT_POINT}} missing or empty"
            exit 1
        fi
        if ! grep -qE '^PasswordAuthentication[[:space:]]+no' "$_drop_in" \
            || ! grep -qE '^PermitRootLogin[[:space:]]+no' "$_drop_in" \
            || ! grep -qE '^KbdInteractiveAuthentication[[:space:]]+no' "$_drop_in"; then
            echo "ERROR: sshd drop-in does not disable password, keyboard-interactive, and root login"
            exit 1
        fi
        if ! grep -Eq '^[[:space:]]*Include[[:space:]]+/etc/ssh/sshd_config\.d/\*\.conf([[:space:]]|$)' "${MOUNT_POINT}/etc/ssh/sshd_config"; then
            echo "ERROR: sshd_config does not include /etc/ssh/sshd_config.d/*.conf"
            exit 1
        fi
    fi

    install -m 0644 "${LIVE_SESSION_DIR}/pf-live.conf" "${MOUNT_POINT}/etc/pf.conf"
    if grep -q '^[^#].*\<log\>' "${MOUNT_POINT}/etc/pf.conf"; then
        echo "ERROR: live PF ruleset unexpectedly enables logging"
        exit 1
    fi

    install -m 0755 "${LIVE_SESSION_DIR}/clawdie-tailscale-up" \
        "${MOUNT_POINT}/usr/local/etc/rc.d/clawdie_tailscale_up"
    rm -f "${MOUNT_POINT}/var/lib/clawdie-iso/tailscale-authkey"
    if [ -n "${TAILSCALE_AUTHKEY:-}" ]; then
        mkdir -p "${MOUNT_POINT}/var/lib/clawdie-iso"
        printf '%s\n' "${TAILSCALE_AUTHKEY}" > "${MOUNT_POINT}/var/lib/clawdie-iso/tailscale-authkey"
        chmod 0600 "${MOUNT_POINT}/var/lib/clawdie-iso/tailscale-authkey"
        set_config_line "${MOUNT_POINT}/etc/rc.conf" 'clawdie_tailscale_up_enable="YES"'
    else
        set_config_line "${MOUNT_POINT}/etc/rc.conf" 'clawdie_tailscale_up_enable="NO"'
    fi

    # Keep ZFS tools/modules ready on the live USB, but do not let a copied or
    # stale zpool cache auto-import disks from the machine being rescued. The
    # operator can still import pools explicitly with mdo when needed.
    mkdir -p "${MOUNT_POINT}/etc/zfs" "${MOUNT_POINT}/boot/zfs"
    rm -f "${MOUNT_POINT}/etc/zfs/zpool.cache" "${MOUNT_POINT}/boot/zfs/zpool.cache"

    # Xorg's XKB compiler must be able to chdir into the XKB root and write a
    # compiled keymap cache. The stock memstick/rootless-X path is unforgiving:
    # if /tmp, /var/tmp, /var/lib/xkb, or any XKB parent directory is missing or
    # not traversable, xkbcomp reports a misleading missing keycodes file.
    mkdir -p "${MOUNT_POINT}/tmp" "${MOUNT_POINT}/var/tmp" "${MOUNT_POINT}/var/lib/xkb"
    chmod 1777 "${MOUNT_POINT}/tmp" "${MOUNT_POINT}/var/tmp" "${MOUNT_POINT}/var/lib/xkb"
    mkdir -p "${MOUNT_POINT}/var/run/user" "${MOUNT_POINT}/var/run/user/${CLAWDIE_UID}"
    chroot "${MOUNT_POINT}" chown clawdie:clawdie "/var/run/user/${CLAWDIE_UID}"
    chmod 0755 "${MOUNT_POINT}/var/run/user"
    chmod 0700 "${MOUNT_POINT}/var/run/user/${CLAWDIE_UID}"
    for _xkb_dir in \
        "${MOUNT_POINT}/usr" \
        "${MOUNT_POINT}/usr/local" \
        "${MOUNT_POINT}/usr/local/share" \
        "${MOUNT_POINT}/usr/local/share/X11" \
        "${MOUNT_POINT}/usr/local/share/X11/xkb" \
        "${MOUNT_POINT}/usr/local/share/X11/xkb/keycodes" \
        "${MOUNT_POINT}/usr/local/share/X11/xkb/rules" \
        "${MOUNT_POINT}/usr/local/share/X11/xkb/symbols" \
        "${MOUNT_POINT}/usr/local/share/X11/xkb/types" \
        "${MOUNT_POINT}/usr/local/share/X11/xkb/compat"; do
        if [ ! -d "$_xkb_dir" ]; then
            echo "ERROR: XKB directory missing from live image: ${_xkb_dir#${MOUNT_POINT}}"
            exit 1
        fi
        chmod 0755 "$_xkb_dir"
    done
    if [ ! -f "${MOUNT_POINT}/usr/local/share/X11/xkb/keycodes/xfree86" ]; then
        echo "ERROR: XKB keycodes missing from live image"
        exit 1
    fi
    if [ ! -x "${MOUNT_POINT}/usr/local/bin/xkbcomp" ]; then
        echo "ERROR: xkbcomp missing from live image"
        exit 1
    fi
    _xkb_test="${CACHE_DIR}/xkb-default-test.xkb"
    _xkb_out="${CACHE_DIR}/xkb-default-test.xkm"
    cat > "$_xkb_test" <<'EOF'
xkb_keymap {
  xkb_keycodes { include "xfree86+aliases(qwerty)" };
  xkb_types { include "complete" };
  xkb_compat { include "complete" };
  xkb_symbols { include "pc+us+inet(evdev)" };
  xkb_geometry { include "pc(pc105)" };
};
EOF
    if ! "${MOUNT_POINT}/usr/local/bin/xkbcomp" -w 0 -R"${MOUNT_POINT}/usr/local/share/X11/xkb" "$_xkb_test" "$_xkb_out" >/dev/null 2>&1; then
        echo "ERROR: XKB default keymap does not compile against the live image xkeyboard-config"
        exit 1
    fi
    rm -f "$_xkb_test" "$_xkb_out"

    _xkb_si_test="${CACHE_DIR}/xkb-si-test.xkb"
    _xkb_si_out="${CACHE_DIR}/xkb-si-test.xkm"
    cat > "$_xkb_si_test" <<'EOF'
xkb_keymap {
  xkb_keycodes { include "xfree86+aliases(qwerty)" };
  xkb_types { include "complete" };
  xkb_compat { include "complete" };
  xkb_symbols { include "pc+si+inet(evdev)" };
  xkb_geometry { include "pc(pc105)" };
};
EOF
    if ! "${MOUNT_POINT}/usr/local/bin/xkbcomp" -w 0 -R"${MOUNT_POINT}/usr/local/share/X11/xkb" "$_xkb_si_test" "$_xkb_si_out" >/dev/null 2>&1; then
        echo "ERROR: Slovenian (si) XKB keymap does not compile — missing xkeyboard-config layout?"
        exit 1
    fi
    rm -f "$_xkb_si_test" "$_xkb_si_out"

    # Preseed XFCE panel layout into /etc/skel so the clawdie user gets it
    # on first login. pw useradd -m copies /etc/skel into /home/clawdie, but
    # the user was already created above, so copy directly into both locations.
    PANEL_SKEL="${LIVE_SESSION_DIR}/panel-skel"
    if [ -d "$PANEL_SKEL" ]; then
        echo "    Seeding XFCE panel layout..."
        mkdir -p "${MOUNT_POINT}/usr/local/share/clawdie-iso/panel-skel"
        cp -R "${PANEL_SKEL}/." "${MOUNT_POINT}/usr/local/share/clawdie-iso/panel-skel/"
        mkdir -p "${MOUNT_POINT}/usr/local/etc/xdg/xfce4/xfconf/xfce-perchannel-xml"
        cp "${PANEL_SKEL}/.config/xfce4/xfconf/xfce-perchannel-xml/xfce4-panel.xml" \
            "${MOUNT_POINT}/usr/local/etc/xdg/xfce4/xfconf/xfce-perchannel-xml/xfce4-panel.xml"
        for _xfconf_xml in xsettings.xml xfwm4.xml xfce4-desktop.xml xfce4-power-manager.xml displays.xml; do
            if [ -f "${PANEL_SKEL}/.config/xfce4/xfconf/xfce-perchannel-xml/${_xfconf_xml}" ]; then
                cp "${PANEL_SKEL}/.config/xfce4/xfconf/xfce-perchannel-xml/${_xfconf_xml}" \
                    "${MOUNT_POINT}/usr/local/etc/xdg/xfce4/xfconf/xfce-perchannel-xml/${_xfconf_xml}"
            fi
        done
        # Install into /etc/skel for any future users
        _skel_etc="${MOUNT_POINT}/etc/skel"
        mkdir -p "${_skel_etc}/.config"
        cp -R "${PANEL_SKEL}/.config/." "${_skel_etc}/.config/"
        if [ -d "${PANEL_SKEL}/.local/share/applications" ]; then
            mkdir -p "${_skel_etc}/.local/share/applications"
            cp "${PANEL_SKEL}/.local/share/applications/mimeapps.list" \
                "${_skel_etc}/.local/share/applications/" 2>/dev/null || true
        fi
        # Also copy directly into clawdie's home (user already exists)
        mkdir -p "${MOUNT_POINT}/home/clawdie/.config"
        cp -R "${PANEL_SKEL}/.config/." "${MOUNT_POINT}/home/clawdie/.config/"
        if [ -d "${PANEL_SKEL}/.local/share/applications" ]; then
            mkdir -p "${MOUNT_POINT}/home/clawdie/.local/share/applications"
            cp "${PANEL_SKEL}/.local/share/applications/mimeapps.list" \
                "${MOUNT_POINT}/home/clawdie/.local/share/applications/" 2>/dev/null || true
        fi
        chroot "$MOUNT_POINT" chown -R clawdie:clawdie /home/clawdie/.config
        chroot "$MOUNT_POINT" chown -R clawdie:clawdie /home/clawdie/.local
        chroot "$MOUNT_POINT" chown -R root:wheel /etc/skel
    fi

    _wallpapers_dir="${LIVE_SESSION_DIR}/wallpapers"
    if [ -d "$_wallpapers_dir" ]; then
        echo "    Installing wallpapers..."
        mkdir -p "${MOUNT_POINT}/usr/local/share/clawdie-iso/wallpapers"
        cp "${_wallpapers_dir}"/* "${MOUNT_POINT}/usr/local/share/clawdie-iso/wallpapers/" 2>/dev/null || true
    fi

    # Brand icons (e.g. the Whisker Start-button triangle). Install both the
    # raw files for direct diagnostics and hicolor theme names for XFCE plugins
    # that do not reliably load absolute icon paths.
    _icons_dir="${LIVE_SESSION_DIR}/icons"
    if [ -d "$_icons_dir" ]; then
        echo "    Installing brand icons..."
        mkdir -p \
            "${MOUNT_POINT}/usr/local/share/clawdie-iso/icons" \
            "${MOUNT_POINT}/usr/local/share/icons/hicolor/48x48/apps" \
            "${MOUNT_POINT}/usr/local/share/icons/hicolor/64x64/apps" \
            "${MOUNT_POINT}/usr/local/share/icons/hicolor/scalable/apps"
        cp "${_icons_dir}"/* "${MOUNT_POINT}/usr/local/share/clawdie-iso/icons/" 2>/dev/null || true
        if [ -f "${_icons_dir}/clawdie-start-48.png" ]; then
            install -m 0644 "${_icons_dir}/clawdie-start-48.png" \
                "${MOUNT_POINT}/usr/local/share/icons/hicolor/48x48/apps/clawdie-start.png"
        fi
        if [ -f "${_icons_dir}/clawdie-start.png" ]; then
            install -m 0644 "${_icons_dir}/clawdie-start.png" \
                "${MOUNT_POINT}/usr/local/share/icons/hicolor/64x64/apps/clawdie-start.png"
        fi
        if [ -f "${_icons_dir}/clawdie-start.svg" ]; then
            install -m 0644 "${_icons_dir}/clawdie-start.svg" \
                "${MOUNT_POINT}/usr/local/share/icons/hicolor/scalable/apps/clawdie-start.svg"
        fi
        if [ -x "${MOUNT_POINT}/usr/local/bin/gtk-update-icon-cache" ]; then
            run_live_chroot /usr/local/bin/gtk-update-icon-cache -f -t /usr/local/share/icons/hicolor || true
        fi
    fi

    rm -rf "${MOUNT_POINT}/home/clawdie/.cache" "${MOUNT_POINT}/etc/skel/.cache"
    ln -s /tmp/clawdie/cache "${MOUNT_POINT}/home/clawdie/.cache"
    ln -s /tmp/clawdie/cache "${MOUNT_POINT}/etc/skel/.cache"
    chroot "${MOUNT_POINT}" chown -h clawdie:clawdie /home/clawdie/.cache
}

if [ "${BUILD_CHANNEL}" = "release" ]; then
    check_release_gate
fi
preflight_colibri_artifacts
preflight_zot_artifacts

# --- step 1: fetch FreeBSD memstick ---
MEMSTICK="${CACHE_DIR}/FreeBSD-${FREEBSD_VERSION}-${FREEBSD_ARCH}-memstick.img"
if [ "$SKIP_MEMSTICK_FETCH" -eq 1 ]; then
    if [ ! -f "$MEMSTICK" ]; then
        echo "ERROR: cached FreeBSD memstick not found: ${MEMSTICK}"
        echo "Rerun without --skip-fetch / --skip-fetch-memstick to download it."
        exit 1
    fi
    if ! verify_memstick_cache; then
        echo "ERROR: cached FreeBSD memstick failed checksum verification."
        echo "Remove ${MEMSTICK} and rerun without --skip-fetch / --skip-fetch-memstick."
        exit 1
    fi
    echo "==> [1/7] FreeBSD memstick cached; fetch skipped."
else
    echo "==> [1/7] Fetching FreeBSD memstick..."
    mkdir -p "$CACHE_DIR"
    curl -L --progress-bar -o "$MEMSTICK" "$FREEBSD_MEMSTICK_URL"
    curl -L -o "${MEMSTICK}.SHA256" "$FREEBSD_MEMSTICK_SHA256_URL"
    verify_memstick_cache || { echo "ERROR: checksum mismatch on memstick"; exit 1; }
fi

# --- step 2: fetch all packages (no root needed) ---
if [ "$SKIP_PKG_FETCH" -eq 0 ]; then
    echo "==> [2/7] Fetching packages to tmp/packages/..."
    mkdir -p "$PKG_REPO_DIR"

    # Set pkg repo to configured branch before fetching
    # Use temporary user-level config to avoid requiring root
    PKG_CONFIG_DIR=$(mktemp -d "${TMP_DIR}/pkg-repo.XXXXXX")
    trap "rm -rf $PKG_CONFIG_DIR" EXIT
    # Avoid calling pkg config (needs privilege) — hardcode ABI and use cache
    ABI="FreeBSD:15:amd64"
    PKG_CACHE_HOME="${HOME}/.pkg-cache"
    mkdir -p "$PKG_CONFIG_DIR" "$PKG_CACHE_HOME"
    cat > "$PKG_CONFIG_DIR/FreeBSD.conf" <<EOF
FreeBSD: {
  url: "pkg+https://pkg.FreeBSD.org/${ABI}/${DEFAULT_PKG_BRANCH}",
  mirror_type: "srv",
  enabled: yes
}
EOF

    PKGS=$(pkg_list_all)
    PKG_COUNT=$(echo "$PKGS" | wc -l | tr -d ' ')
    echo "    Fetching ${PKG_COUNT} packages (with dependencies)..."

    # pkg fetch downloads .pkg files and all dependencies to tmp/packages/
    # Use PKG_REPOS_DIR and PKG_CACHEDIR to minimize privilege requirements
    export PKG_REPOS_DIR="$PKG_CONFIG_DIR"
    export PKG_CACHEDIR="$PKG_CACHE_HOME"

    # Try unprivileged fetch first
    _PKG_FETCH_ERROR="${TMP_DIR}/pkg-fetch-error.log"
    if ! echo "$PKGS" | xargs pkg fetch --yes --dependencies --output "$PKG_REPO_DIR" 2>"$_PKG_FETCH_ERROR"; then
        # If privilege error, offer to re-run with sudo
        if grep -q "Insufficient privileges" "$_PKG_FETCH_ERROR" 2>/dev/null; then
            echo "    ⚠️  pkg fetch requires elevated privileges."
            echo "    Re-running with sudo..."
            sudo sh -c "export PKG_REPOS_DIR='$PKG_CONFIG_DIR' PKG_CACHEDIR='$PKG_CACHE_HOME'; echo '$PKGS' | xargs pkg fetch --yes --dependencies --output '$PKG_REPO_DIR'" || {
                echo "    ERROR: sudo pkg fetch failed"
                exit 1
            }
        else
            cat "$_PKG_FETCH_ERROR"
            exit 1
        fi
    fi

    echo "    Fetch complete."

    # Fetch pinned npm-global CLI tarballs for the live image bundle.
    echo "==> [2b/7] Fetching npm-global CLI tarballs..."
    OUT_DIR="$NPM_GLOBALS_DIR" "${SCRIPT_DIR}/scripts/fetch-npm-globals.sh" || {
        echo "    ERROR: fetch-npm-globals.sh failed"
        exit 1
    }
else
    echo "==> [2/7] Skipping package fetch."
fi

if [ -d "${PKG_REPO_DIR}/Hashed" ]; then
    mkdir -p "${PKG_REPO_DIR}/All/Hashed"
    find "${PKG_REPO_DIR}/Hashed" -maxdepth 1 -type f -name '*.pkg' -exec mv {} "${PKG_REPO_DIR}/All/Hashed/" \;
    rmdir "${PKG_REPO_DIR}/Hashed" 2>/dev/null || true
fi

if [ -d "${PKG_REPO_DIR}/All/Hashed" ]; then
    override_networkmgr_package
fi

# --- step 3: generate offline pkg repo metadata ---
echo "==> [3/7] Generating offline pkg repo metadata..."
if [ -d "$PKG_REPO_DIR/All" ]; then
    # pkg repo may also need privilege — try unprivileged first
    _PKG_REPO_ERROR="${TMP_DIR}/pkg-repo-error.log"
    if ! pkg repo "$PKG_REPO_DIR" 2>"$_PKG_REPO_ERROR"; then
        if grep -q "Insufficient privileges" "$_PKG_REPO_ERROR" 2>/dev/null; then
            echo "    ⚠️  pkg repo requires elevated privileges. Re-running with sudo..."
            sudo pkg repo "$PKG_REPO_DIR" || {
                echo "    ERROR: sudo pkg repo failed"
                exit 1
            }
        else
            cat "$_PKG_REPO_ERROR"
            exit 1
        fi
    fi
    echo "    Repo metadata written to tmp/packages/"
else
    echo "    WARN: tmp/packages/All/ not found — run without --skip-fetch / --skip-fetch-pkg first"
fi

# --- step 4: fetch + prepare Clawdie-AI tarball (offline-ready) ---
# Resolve "latest" from Forgejo releases first, then tags.
if [ "${CLAWDIE_REF:-${CLAWDIE_VERSION:-}}" = "latest" ] || [ -z "${CLAWDIE_REF:-}" ]; then
    echo "==> [4/7] Resolving latest Clawdie-AI version..."
    CLAWDIE_VERSION=$(resolve_latest_clawdie_tag | sed 's/^v//')
    if [ -z "$CLAWDIE_VERSION" ]; then
        echo "ERROR: could not resolve latest Clawdie-AI release/tag from Forgejo."
        echo "       Pin --clawdie-ref main or --clawdie-version X.Y.Z explicitly."
        exit 1
    fi
    CLAWDIE_REF="v${CLAWDIE_VERSION}"
    echo "    Resolved: ${CLAWDIE_REF}"
fi

CLAWDIE_AI_COMMIT=$(resolve_clawdie_commit "$CLAWDIE_REF" | head -n 1)
CLAWDIE_AI_COMMIT="${CLAWDIE_AI_COMMIT:-unknown}"
echo "    Clawdie commit: ${CLAWDIE_AI_COMMIT}"

if [ "$CLAWDIE_AI_COMMIT" = "unknown" ] && [ "$SKIP_PKG_FETCH" -eq 1 ] && ! is_pinned_clawdie_ref "$CLAWDIE_REF"; then
    echo "ERROR: cannot safely skip the fetch for moving Clawdie-AI ref '${CLAWDIE_REF}' without resolving its commit."
    echo "       Run without --skip-fetch / --skip-fetch-pkg, or pin --clawdie-version / --clawdie-ref to a commit."
    exit 1
fi

if [ "$CLAWDIE_AI_COMMIT" != "unknown" ]; then
    CLAWDIE_ARCHIVE_REF="$CLAWDIE_AI_COMMIT"
    CLAWDIE_CACHE_KEY="$CLAWDIE_AI_COMMIT"
else
    CLAWDIE_ARCHIVE_REF="$CLAWDIE_REF"
    CLAWDIE_CACHE_KEY=$(printf '%s' "$CLAWDIE_REF" | tr -c 'A-Za-z0-9._-' '_')
fi

CLAWDIE_TARBALL="${CACHE_DIR}/clawdie-ai-${CLAWDIE_CACHE_KEY}.tar.gz"
CLAWDIE_TARBALL_URL="https://code.smilepowered.org/clawdie/clawdie-ai/archive/${CLAWDIE_ARCHIVE_REF}.tar.gz"
if [ "$SKIP_PKG_FETCH" -eq 0 ] || [ ! -f "$CLAWDIE_TARBALL" ]; then
    echo "==> [4/7] Fetching Clawdie-AI ${CLAWDIE_REF} (${CLAWDIE_ARCHIVE_REF})..."
    mkdir -p "$CACHE_DIR"
    curl -L --progress-bar -o "$CLAWDIE_TARBALL" "$CLAWDIE_TARBALL_URL"
else
    echo "==> [4/7] Clawdie-AI ${CLAWDIE_REF} (${CLAWDIE_ARCHIVE_REF}) cached."
fi

# Build an ISO-ready tarball that includes node_modules for offline firstboot.
CLAWDIE_TARBALL_ISO="${CACHE_DIR}/clawdie-ai-${CLAWDIE_CACHE_KEY}-iso.tar.gz"
if [ "$SKIP_PKG_FETCH" -eq 0 ] || [ ! -f "$CLAWDIE_TARBALL_ISO" ]; then
    echo "==> [4/7] Preparing Clawdie-AI offline tarball (node_modules)..."

    if ! command -v npm >/dev/null 2>&1; then
        echo "ERROR: npm not found on build host."
        echo "Install Node.js + npm on the ISO build machine, then rerun build.sh."
        echo "Example (FreeBSD): sudo pkg install -y node24 npm-node24"
        exit 1
    fi

    BUILD_AI_DIR="${CACHE_DIR}/clawdie-ai-build"
    STAGE_AI_DIR="${CACHE_DIR}/clawdie-ai-stage"
    rm -rf "$BUILD_AI_DIR"
    rm -rf "$STAGE_AI_DIR"
    mkdir -p "$BUILD_AI_DIR"
    mkdir -p "$STAGE_AI_DIR"

    tar -xzf "$CLAWDIE_TARBALL" -C "$BUILD_AI_DIR"

    AI_SRC_DIR="$(find "$BUILD_AI_DIR" -mindepth 1 -maxdepth 1 -type d | head -n 1)"
    if [ -z "${AI_SRC_DIR:-}" ]; then
        echo "ERROR: Could not locate extracted Clawdie-AI directory."
        exit 1
    fi

    _bundle_script="${CACHE_DIR}/bundle-clawdie-ai.sh"
    cat > "$_bundle_script" <<'EOF'
#!/bin/sh
set -eu
cd "$BUILD_AI_SRC_DIR"
# Never let package-manager lifecycle hooks run the Clawdie installer on
# the ISO build host. Older Clawdie-AI refs used an npm "install" script
# as an operator shortcut; remove only that root lifecycle hook in the
# staged copy while still allowing dependency install scripts.
node -e '
  const fs = require("fs");
  const path = "package.json";
  const pkg = JSON.parse(fs.readFileSync(path, "utf8"));
  if (pkg.scripts?.install) delete pkg.scripts.install;
  fs.writeFileSync(path, JSON.stringify(pkg, null, 2) + "\n");
'
if ! npm ci --no-audit --no-fund --legacy-peer-deps; then
    echo "    WARN: npm ci failed for Clawdie-AI; falling back to npm install."
    echo "    WARN: This usually means the published release tarball is out of sync with package-lock.json."
    npm install --no-audit --no-fund --legacy-peer-deps
fi
EOF
    chmod 700 "$_bundle_script"
    if [ -n "${BUILD_HOST_USER}" ]; then
        chown -R "$BUILD_HOST_USER":"$BUILD_HOST_USER" "$BUILD_AI_DIR" "$STAGE_AI_DIR" "$_bundle_script" 2>/dev/null || true
        su -m "$BUILD_HOST_USER" -c "env HOME='${BUILD_HOST_HOME}' PATH='${PATH}' BUILD_AI_SRC_DIR='${AI_SRC_DIR}' sh '${_bundle_script}'"
    else
        env BUILD_AI_SRC_DIR="${AI_SRC_DIR}" sh "$_bundle_script"
    fi

    mv "$AI_SRC_DIR" "${STAGE_AI_DIR}/Clawdie-AI"

    tar -czf "$CLAWDIE_TARBALL_ISO" -C "$STAGE_AI_DIR" Clawdie-AI
else
    echo "==> [4/7] Clawdie-AI offline tarball cached."
fi

# Exit here if --fetch-only (CI package pre-fetch step, no root required)
if [ "$FETCH_ONLY" -eq 1 ]; then
    echo ""
    echo "==> Fetch complete. Run ./build.sh --skip-fetch to assemble ISO."
    exit 0
fi

# --- step 5: prepare working image (requires root) ---
echo "==> [5/7] Preparing working image (${IMAGE_SIZE} for offline packages)..."
if [ "$(id -u)" -ne 0 ]; then
    echo "ERROR: steps 5-7 require root (mdconfig/mount)"
    exit 1
fi

WORK_IMG="${CACHE_DIR}/work.img"

# Recreate the working image on every assembly. Package/profile changes must not
# inherit stale packages or desktop files from a previous branch build.
rm -f "$WORK_IMG"

# Create image with configured size for offline packages
if [ ! -f "$WORK_IMG" ]; then
    echo "    Creating ${IMAGE_SIZE} image..."
    truncate -s "${IMAGE_SIZE}" "$WORK_IMG"

    # Attach to mdconfig and partition
    MD=$(mdconfig -a -t vnode -f "$WORK_IMG")
    echo "    Attached as /dev/${MD}"

    # Initialize an MBR layout compatible with modern UEFI removable boot:
    #   s1: EFI System Partition (FAT, contains /EFI/BOOT/BOOTX64.EFI)
    #   s2: FreeBSD slice with BSD label + UFS root
    #   s3: CLAWDIESEED FAT32 partition for operator seed files (SSH keys,
    #       future hostname / Tailscale / Wi-Fi overrides). Lives at the
    #       tail of the disk so Linux pre-flash tooling can mount and edit
    #       it without touching the UFS root. Imported on every boot by
    #       /usr/local/etc/rc.d/clawdie_live_seed.
    _seed_size_m=64
    _efi_size_m=64
    _image_size_m=$(( $(stat -f %z "$WORK_IMG") / 1024 / 1024 ))
    # 4 MiB headroom covers MBR overhead + alignment rounding. Without this
    # the trailing 'gpart add -s ${_seed_size_m}M' can fail when the EFI
    # slice rounds up past the requested 64M.
    _freebsd_size_m=$(( _image_size_m - _efi_size_m - _seed_size_m - 4 ))

    gpart create -s MBR /dev/${MD}
    gpart add -t efi -s ${_efi_size_m}M /dev/${MD}
    gpart add -t freebsd -s ${_freebsd_size_m}M /dev/${MD}
    gpart add -t '!12' -s ${_seed_size_m}M /dev/${MD}   # !12 = FAT32 LBA (0x0c)
    gpart set -a active -i 2 /dev/${MD}

    # Create BSD label (partition a) in the FreeBSD slice
    gpart create -s BSD /dev/${MD}s2
    gpart add -t freebsd-ufs /dev/${MD}s2

    # Create UFS filesystem on partition a. Keep the label expected by the
    # stock FreeBSD memstick loader configuration.
    newfs -U -L FreeBSD_Install /dev/${MD}s2a

    # Format and seed the CLAWDIESEED partition. Operators mount this on
    # Linux (sudo mount -t vfat /dev/sdX3 /mnt/...) pre-flash to drop
    # authorized_keys. The README.txt explains the allowlisted contract.
    #
    # -c 1 forces 1 sector/cluster (512 B). FAT32 requires >=65525 clusters
    # by spec; at default cluster size (4 KiB+) a 64 MiB partition falls
    # well below that and newfs_msdos rejects it ("too few clusters for
    # FAT32, need 65525"). 512 B clusters give us 131072 clusters with a
    # ~512 KiB FAT table — <1% overhead, invisible for the few-KiB text
    # files this partition is meant to hold.
    newfs_msdos -F 32 -c 1 -L CLAWDIESEED /dev/${MD}s3
    _seed_mount="${CACHE_DIR}/seed-mnt"
    mkdir -p "${_seed_mount}"
    mount_msdosfs /dev/${MD}s3 "${_seed_mount}"
    install -m 0644 "${LIVE_SESSION_DIR}/clawdie-live-seed.README.txt" \
        "${_seed_mount}/README.txt"

    # Seed agent directory with operational files.
    # The seed importer (clawdie-live-seed) reads these at boot:
    #   env → merged into ~/.env + /usr/local/etc/colibri/provider.env
    #   AGENTS.md → /var/db/colibri/.local/state/zot/AGENTS.md (zot global slot)
    #   harness.toml → recorded for Colibri runtime.
    #   soul/ → staged for Hermes (dormant, loaded when Hermes is installed).
    _seed_agent_dir="${_seed_mount}/clawdie"
    mkdir -p "${_seed_agent_dir}"

    # Operational rules for the autospawned zot agent.
    if [ -f "${LIVE_SESSION_DIR}/seed/AGENTS.md" ]; then
        install -m 0644 "${LIVE_SESSION_DIR}/seed/AGENTS.md" \
            "${_seed_agent_dir}/AGENTS.md"
    fi

    # Harness config.
    if [ -f "${LIVE_SESSION_DIR}/seed/harness.toml" ]; then
        install -m 0644 "${LIVE_SESSION_DIR}/seed/harness.toml" \
            "${_seed_agent_dir}/harness.toml"
    fi

    # Provider keys: read from the build host's provider.env, write only
    # the API key lines to the seed.  The seed importer merges them into
    # both the operator's ~/.env and the daemon's provider.env at boot.
    if [ -f /usr/local/etc/colibri/provider.env ]; then
        grep -E '^(DEEPSEEK_API_KEY|OPENROUTER_API_KEY|TELEGRAM_BOT_TOKEN)=' \
            /usr/local/etc/colibri/provider.env \
            > "${_seed_agent_dir}/env" 2>/dev/null || true
        if [ -s "${_seed_agent_dir}/env" ]; then
            chmod 0600 "${_seed_agent_dir}/env"
            echo "    Seeded $(wc -l < "${_seed_agent_dir}/env") API key(s) from provider.env"
        fi
    fi

    # Layered-soul identity files (dormant — for Hermes later).
    if [ -d /home/clawdie/ai/layered-soul ]; then
        mkdir -p "${_seed_agent_dir}/soul"
        cp -R /home/clawdie/ai/layered-soul/. "${_seed_agent_dir}/soul/" 2>/dev/null || true
        rm -rf "${_seed_agent_dir}/soul/.git" 2>/dev/null || true
        echo "    Seeded soul backup (layered-soul)"
    fi

    sync
    umount "${_seed_mount}"
    rmdir "${_seed_mount}" 2>/dev/null || true

    # Mount the new filesystem
    MOUNT_POINT="${CACHE_DIR}/mnt"
    mkdir -p "$MOUNT_POINT"
    mount /dev/${MD}s2a "$MOUNT_POINT"
    echo "    Mounted /dev/${MD}s2a at ${MOUNT_POINT}"

    # Mount memstick read-only to extract base system
    MEMSTICK_MNT="${CACHE_DIR}/memstick-src"
    mkdir -p "$MEMSTICK_MNT"
    mount_memstick_rootfs "$MEMSTICK_MNT"
    echo "    Copying base system from memstick..."

    # Copy all files from memstick to new image (excluding package cache)
    (
        cd "$MEMSTICK_MNT"
        pax -rw -pe . "$MOUNT_POINT"
    )

    # Cleanup memstick mount
    cleanup_memstick_rootfs "$MEMSTICK_MNT"
    rm -rf "$MEMSTICK_MNT"

    install_image_bootcode "$MD" "$MOUNT_POINT" "2"
    install_image_uefi_bootcode "$MD" "$MOUNT_POINT" "1"

    # Store MD device for later cleanup
    echo "$MD" > "${CACHE_DIR}/.md_device"
else
    # Reattach existing image (but extract base system if empty)
    MD=$(mdconfig -a -t vnode -f "$WORK_IMG")
    echo "    Reattached as /dev/${MD}"

    MOUNT_POINT="${CACHE_DIR}/mnt"
    mkdir -p "$MOUNT_POINT"
    if [ -e "/dev/${MD}s2a" ]; then
        ROOT_PART="/dev/${MD}s2a"
        ROOT_SLICE_INDEX="2"
    else
        ROOT_PART="/dev/${MD}s1a"
        ROOT_SLICE_INDEX="1"
    fi
    mount "$ROOT_PART" "$MOUNT_POINT"
    echo "    Mounted ${ROOT_PART} at ${MOUNT_POINT}"

    # Check if base system is missing and extract if needed
    if [ ! -d "$MOUNT_POINT/etc" ]; then
        echo "    Extracting base system from memstick..."
        MEMSTICK_MNT="${CACHE_DIR}/memstick-src"
        mkdir -p "$MEMSTICK_MNT"
        mount_memstick_rootfs "$MEMSTICK_MNT"

        (
            cd "$MEMSTICK_MNT"
            pax -rw -pe . "$MOUNT_POINT"
        )

        cleanup_memstick_rootfs "$MEMSTICK_MNT"
        rm -rf "$MEMSTICK_MNT"

        install_image_bootcode "$MD" "$MOUNT_POINT" "$ROOT_SLICE_INDEX"
        if [ "$ROOT_SLICE_INDEX" = "2" ]; then
            install_image_uefi_bootcode "$MD" "$MOUNT_POINT" "1"
        fi
    fi

    # Store MD device for later cleanup
    echo "$MD" > "${CACHE_DIR}/.md_device"
fi

# --- step 6: inject payload ---
echo "==> [6/7] Injecting payload..."

# Create share directory on USB
USB_SHARE="${MOUNT_POINT}/usr/local/share/clawdie-iso"
mkdir -p "$USB_SHARE"

# Step 1 switches the live boot entrypoint away from auto-running
# /etc/installerconfig. Keep the script in USB_SHARE for later explicit
# bsdinstall invocation, but remove any legacy boot-time auto script.
rm -f "${MOUNT_POINT}/etc/installerconfig"

install_live_runtime_packages
install_firefox_extensions
configure_live_operator_session
install_colibri_service
install_zot_agent
install_nvidia_universal_repo
build_and_stage_docs

# Copy payload
# Rebuild payload paths from scratch inside the reusable work image. A failed
# prior build can leave a partial package tree behind, and overlaying a new
# hashed pkg repo onto that stale path produces "Not a directory" copy errors.
rm -rf "${USB_SHARE}/firstboot" "${USB_SHARE}/packages" "${USB_SHARE}/npm-globals"
rm -f "${USB_SHARE}/installerconfig" \
      "${USB_SHARE}/clawdie-ai.tar.gz" \
      "${USB_SHARE}/build.cfg" \
      "${USB_SHARE}/build-manifest.json"

cp "${SCRIPT_DIR}/installerconfig" "${USB_SHARE}/installerconfig"
mkdir -p "${USB_SHARE}/firstboot"
(
    cd "${SCRIPT_DIR}/firstboot"
    pax -rw -pe . "${USB_SHARE}/firstboot"
)
mkdir -p "${USB_SHARE}/packages"
(
    cd "${PKG_REPO_DIR}"
    pax -rw -pe . "${USB_SHARE}/packages"
)
if [ -d "$NPM_GLOBALS_DIR" ]; then
    mkdir -p "${USB_SHARE}/npm-globals"
    (
        cd "${NPM_GLOBALS_DIR}"
        pax -rw -pe . "${USB_SHARE}/npm-globals"
    )
    NPM_GLOBAL_TARBALL_COUNT=$(find "${NPM_GLOBALS_DIR}" -maxdepth 1 -type f -name '*.tgz' | wc -l | tr -d ' ')
    echo "    Bundled npm-globals: ${NPM_GLOBAL_TARBALL_COUNT} tarballs"
fi
install_live_npm_globals
cp "${CLAWDIE_TARBALL_ISO}" "${USB_SHARE}/clawdie-ai.tar.gz"
cp "${SCRIPT_DIR}/build.cfg" "${USB_SHARE}/"
write_build_manifest "${USB_SHARE}/build-manifest.json"

# Bake runtime vars so firstboot reads the right target config
{
    echo "ISO_VERSION=\"${ISO_VERSION}\""
    echo "BUILD_CHANNEL=\"${BUILD_CHANNEL}\""
    echo "CLAWDIE_VERSION=\"${CLAWDIE_VERSION}\""
    echo "CLAWDIE_REF=\"${CLAWDIE_REF}\""
    echo "CLAWDIE_AI_COMMIT=\"${CLAWDIE_AI_COMMIT}\""
    echo "TARGET=\"${TARGET:-baremetal}\""
    [ -n "${GPU_DRIVER:-}" ]            && echo "GPU_DRIVER=\"${GPU_DRIVER}\""
    [ -n "${ASSISTANT_NAME:-}" ]        && echo "ASSISTANT_NAME=\"${ASSISTANT_NAME}\""
    [ -n "${AGENT_GENDER:-}" ]          && echo "AGENT_GENDER=\"${AGENT_GENDER}\""
    [ -n "${AGENT_DOMAIN:-}" ]          && echo "AGENT_DOMAIN=\"${AGENT_DOMAIN}\""
    [ -n "${TZ:-}" ]                    && echo "TZ=\"${TZ}\""
    [ -n "${PI_TUI_PROVIDER:-}" ]       && echo "PI_TUI_PROVIDER=\"${PI_TUI_PROVIDER}\""
    [ -n "${PI_TUI_MODEL:-}" ]          && echo "PI_TUI_MODEL=\"${PI_TUI_MODEL}\""
    [ -n "${ZAI_API_KEY:-}" ]           && echo "ZAI_API_KEY=\"${ZAI_API_KEY}\""
    [ -n "${OPENROUTER_API_KEY:-}" ]    && echo "OPENROUTER_API_KEY=\"${OPENROUTER_API_KEY}\""
    [ -n "${ANTHROPIC_API_KEY:-}" ]     && echo "ANTHROPIC_API_KEY=\"${ANTHROPIC_API_KEY}\""
    [ -n "${EMBED_BASE_URL:-}" ]        && echo "EMBED_BASE_URL=\"${EMBED_BASE_URL}\""
    [ -n "${EMBED_MODEL:-}" ]           && echo "EMBED_MODEL=\"${EMBED_MODEL}\""
    [ -n "${TELEGRAM_BOT_TOKEN:-}" ]    && echo "TELEGRAM_BOT_TOKEN=\"${TELEGRAM_BOT_TOKEN}\""
    [ -n "${TELEGRAM_ADMIN_IDS:-}" ]    && echo "TELEGRAM_ADMIN_IDS=\"${TELEGRAM_ADMIN_IDS}\""
    [ -n "${FEATURE_TELEGRAM:-}" ]      && echo "FEATURE_TELEGRAM=\"${FEATURE_TELEGRAM}\""
    [ -n "${FEATURE_COLIBRI:-}" ]       && echo "FEATURE_COLIBRI=\"${FEATURE_COLIBRI}\""
    [ -n "${COLIBRI_STAGE_TEST_AGENT:-}" ] && echo "COLIBRI_STAGE_TEST_AGENT=\"${COLIBRI_STAGE_TEST_AGENT}\""
    [ -n "${COLIBRI_DAEMON_ENABLE:-}" ] && echo "COLIBRI_DAEMON_ENABLE=\"${COLIBRI_DAEMON_ENABLE}\""
    [ -n "${COLIBRI_COST_MODE:-}" ]     && echo "COLIBRI_COST_MODE=\"${COLIBRI_COST_MODE}\""
    [ -n "${SSH_PUBLIC_KEY:-}" ]        && echo "SSH_PUBLIC_KEY=\"${SSH_PUBLIC_KEY}\""
    [ -n "${ROOT_PASSWORD:-}" ]         && echo "ROOT_PASSWORD=\"${ROOT_PASSWORD}\""
    [ -n "${CLAWDIE_USER_PASSWORD:-}" ] && echo "CLAWDIE_USER_PASSWORD=\"${CLAWDIE_USER_PASSWORD}\""
} >> "${USB_SHARE}/build.cfg"

echo "    Payload injected."

# Image-size headroom guard. The live filesystem is fixed at IMAGE_SIZE; rust + go
# + offline repo staging + the package cache can fill it. Report usage and fail
# *before* shipping an image that booted with no room to breathe, rather than
# discovering it on the stick. Override the floor with IMAGE_MIN_FREE_MB.
echo "==> Image space report (mounted live filesystem):"
df -h "${MOUNT_POINT}" || true
echo "    Largest staged trees:"
du -sh "${MOUNT_POINT}/usr/local" "${MOUNT_POINT}/home" "${MOUNT_POINT}/var" 2>/dev/null || true
_img_free_kb=$(df -k "${MOUNT_POINT}" | awk 'NR==2 {print $4}')
_img_min_free_mb="${IMAGE_MIN_FREE_MB:-1024}"
if [ -n "${_img_free_kb:-}" ] && [ "${_img_free_kb}" -lt $((_img_min_free_mb * 1024)) ]; then
    echo "ERROR: only $((_img_free_kb / 1024)) MB free on the live filesystem (< ${_img_min_free_mb} MB floor)."
    echo "       Raise IMAGE_SIZE in build.cfg (e.g. for a larger USB) or trim staged content,"
    echo "       or lower the floor with IMAGE_MIN_FREE_MB if you know it fits."
    exit 1
fi
echo "    Free space OK ($((_img_free_kb / 1024)) MB, floor ${_img_min_free_mb} MB)."

# Unmount and detach
umount "$MOUNT_POINT"
if [ -f "${CACHE_DIR}/.md_device" ]; then
    MD=$(cat "${CACHE_DIR}/.md_device")
    mdconfig -d -u "$MD"
    rm "${CACHE_DIR}/.md_device"
fi

# --- step 7: write output ---
echo "==> [7/7] Writing output image..."
mkdir -p "$OUTPUT_DIR"
cp "$WORK_IMG" "${OUTPUT_DIR}/${IMAGE_NAME}"
sync
echo ""
OUTPUT_IMAGE="${OUTPUT_DIR}/${IMAGE_NAME}"
IMAGE_LOGICAL_SIZE=$(ls -lh "$OUTPUT_IMAGE" | awk '{print $5}')
IMAGE_ALLOCATED_SIZE=$(du -sh "$OUTPUT_IMAGE" | awk '{print $1}')
echo "    Done       : ${OUTPUT_IMAGE}"
echo "    Image size : ${IMAGE_LOGICAL_SIZE}"
echo "    Allocated  : ${IMAGE_ALLOCATED_SIZE} (sparse on build host)"
echo ""
echo "    Write to USB:"
echo "    dd if=${OUTPUT_IMAGE} of=/dev/daX bs=1M status=progress conv=fsync && sync"
